--- url: https://open-spaced-repetition.github.io/ts-fsrs/guide/index.md --- > For AI agents: the complete documentation index is available at https://open-spaced-repetition.github.io/ts-fsrs/llms.txt, the full documentation bundle is available at https://open-spaced-repetition.github.io/ts-fsrs/llms-full.txt. # Introduction `ts-fsrs` is a TypeScript toolkit for building spaced repetition systems with FSRS. Its declarative API creates a reusable scheduler definition from a model, a chronology, and optional middleware. ## Building blocks A **model** is the FSRS algorithm and its parameters. `FSRS7Model` is the current one; FSRS-3 through FSRS-6 stay available for review histories built on them. A **chronology** decides how time is represented. `dateChrono` works with `Date`, `numericChrono` with numbers in days, and `temporalInstantChrono` with `Temporal.Instant`. **Middleware** adds scheduling policy, such as `schedulerLearningStepsMiddleware`, `schedulerFuzzingMiddleware`, `schedulerMaximumIntervalMiddleware`, or `schedulerLeechMiddleware`. Add them with `.use(...)` before creating the scheduler. `defineScheduler` combines these into a definition. Calling `.create()` validates the configuration and returns an independent scheduler, so one definition can back several of them. For your Agent Integrate ts-fsrs into a TypeScript application Copy Prompt Integrate ts-fsrs into my TypeScript application. 1. Read the documentation index https://open-spaced-repetition.github.io/ts-fsrs/llms.txt and quick start https://open-spaced-repetition.github.io/ts-fsrs/guide/quick-start.md. First check whether the project already uses ts-fsrs. For a v5 → v6 or FSRS-6 → FSRS-7 upgrade, read the migration guide https://open-spaced-repetition.github.io/ts-fsrs/guide/migration.md and follow its implementation and verification steps, keeping existing cards and logs without rescheduling their next review during migration. The installation and new-card example below apply only to a new integration. 2. Install the `ts-fsrs` package. 3. Create an FSRS-7 scheduler with `defineScheduler`, `FSRS7Model`, and `dateChrono`. 4. Create a new card and review it with `Rating.Good`. 5. Keep the implementation minimal and use the current ts-fsrs public API. Head to [Quick start](https://open-spaced-repetition.github.io/ts-fsrs/guide/quick-start.md) to install the package and run a scheduler in the browser. --- url: https://open-spaced-repetition.github.io/ts-fsrs/guide/quick-start.md --- > For AI agents: the complete documentation index is available at https://open-spaced-repetition.github.io/ts-fsrs/llms.txt, the full documentation bundle is available at https://open-spaced-repetition.github.io/ts-fsrs/llms-full.txt. # Quick start Migrating an existing application? Follow the [v5 → v6 and FSRS-7 migration guide](https://open-spaced-repetition.github.io/ts-fsrs/guide/migration.md). ## Installation `ts-fsrs` requires Node.js 24 or later. ```sh [npm] npm install ts-fsrs ``` ```sh [yarn] yarn add ts-fsrs ``` ```sh [pnpm] pnpm add ts-fsrs ``` ```sh [bun] bun add ts-fsrs ``` ```sh [deno] deno add npm:ts-fsrs ``` Install `@open-spaced-repetition/binding` separately only when you need to train FSRS parameters or convert review-log CSV files. ## Start with DefaultScheduler `DefaultScheduler()` is the fastest way to add FSRS scheduling. It defaults to FSRS-7 and includes the standard retention, statistics, learning-step, maximum-interval, monotonic-interval, and scheduled-days policies; fuzzing is available as an option. ```ts twoslash file="../../snippets/run-code/default-scheduler.ts" import { DefaultScheduler, Rating } from 'ts-fsrs' const now = new Date('2026-01-01T00:00:00.000Z') const scheduler = await DefaultScheduler({ version: 'FSRS-7', desiredRetention: 0.9, enableShortTerm: true, learningSteps: ['1m', '10m'], relearningSteps: ['10m'], enableFuzz: false, maximumInterval: 36_500, }) const card = scheduler.newCard({ cardId: 'quick-start', now }) const result = scheduler.review({ card, grade: Rating.Good, now }) console.log( JSON.stringify( { state: result.card.state, dueAt: result.card.dueAt, stability: result.card.stability, stabilityFast: result.card.stabilityFast, difficulty: result.card.difficulty, rating: result.revlog.rating, }, null, 2 ) ) ``` ▶ Run code The example makes the common defaults explicit. Change `desiredRetention`, learning and relearning steps, fuzzing, or `maximumInterval` for your product; omit `weights` to use the preset for the selected FSRS version. ## Create a scheduler Use `defineScheduler` when you need to choose the model, chronology, or middleware yourself. It creates a reusable definition; calling `.create()` validates the configuration and returns an independent scheduler. `chrono` defines how time is represented, stored on cards and review logs, and used in scheduling calculations, allowing you to use JavaScript `Date`, Temporal, or numeric time values. `chrono.normalize` uses a `ChronoTimeNormalizer` schema to convert card or revlog time fields into `{ previous, current }` for scheduling and rollback. ```ts twoslash file="../../snippets/run-code/define-scheduler.ts" import { dateChrono, defineScheduler, Rating } from 'ts-fsrs' import { FSRS7_DEFAULT_WEIGHTS, FSRS7Model } from 'ts-fsrs/models/fsrs-7' // Reuse this definition as the program's scheduling preset. const schedulerDefinition = defineScheduler({ model: FSRS7Model, chrono: dateChrono, }) // Create an independent scheduler when a user's config is available. const scheduler = schedulerDefinition.create({ config: { weights: FSRS7_DEFAULT_WEIGHTS, fractionalDays: true, }, }) const now = new Date('2026-01-01T00:00:00.000Z') const card = scheduler.newCard({ now }) const result = scheduler.review({ card, grade: Rating.Good, now }) console.log( JSON.stringify( { state: result.card.state, dueAt: result.card.dueAt, stability: result.card.stability, difficulty: result.card.difficulty, stabilityFast: result.card.stabilityFast, rating: result.revlog.rating, }, null, 2 ) ) ``` ▶ Run code :::note Reuse the scheduler definition Keep the result of `defineScheduler(...)`, including any middleware added with `.use(...)`, as a program-level preset. Call `schedulerDefinition.create({ config })` when a user or collection needs a scheduler; each call validates that configuration and returns an independent instance. ::: Use the code block's copy button to copy the original source, or run it directly in the dedicated Worker. The fixed `now` value keeps the output reproducible. - `newCard()` creates the scheduler's default card shape. - `review()` returns a new card and revlog without mutating the input card. - Add policy middleware with `.use(...)` to the reusable definition before creating user-specific schedulers. --- url: https://open-spaced-repetition.github.io/ts-fsrs/guide/migration.md --- > For AI agents: the complete documentation index is available at https://open-spaced-repetition.github.io/ts-fsrs/llms.txt, the full documentation bundle is available at https://open-spaced-repetition.github.io/ts-fsrs/llms-full.txt. # Migrating to v6 and FSRS-7 Package versions and model versions are separate: **ts-fsrs v5 already uses FSRS-6**. First move application code and stored records to the v6 API while keeping FSRS-6. Then migrate the model separately. This guide targets the v6 API in these docs; pin a v6 version that provides the documented interfaces. :::warning Do not use legacy APIs after migration Use `defineScheduler` and the explicit model entry points. Do not use `fsrs()`, `FSRS`, `createEmptyCard`, `generatorParameters`, old scheduler classes/strategies, or legacy types such as the root-exported `Card`, `CardInput`, `ReviewLog` and `FSRSParameters` in migrated code. Some remain exported for compatibility; their presence does not make them the v6 integration path. Old names below are comparison references only. The runnable examples use no legacy API or type. ::: For your Agent Let an Agent complete the migration Copy Prompt Help me migrate the current application to ts-fsrs v6 (defineScheduler). First ask which target I want and wait for my explicit answer. Do not assume a model upgrade or change code or data before I answer: A. Upgrade only to ts-fsrs v6, keeping FSRS-6 and its existing weights and memory states. B. Upgrade to ts-fsrs v6 and upgrade the model from FSRS-6 to FSRS-7. After the user selects a target, perform step 1 read-only and identify the actual changes required for that target. Present an ordered TODO List using - [ ] checkboxes. For each item, identify the affected files or modules, current behavior and proposed change, data impact, and verification method. Ask the user to confirm the list and wait for an explicit reply. Selecting a migration target does not approve the implementation list. Before list approval, do not modify code, configuration or data, or run migration commands with write side effects. Only after the user approves the TODO List, implement its code changes, necessary data migration scripts and tests, checking off each completed item. If additional scope is discovered, update the list and obtain confirmation before executing that additional work. For A, skip steps 6–8 and the FSRS-7-only checks in step 9; do not add stabilityFast or replay history to rebuild FSRS-7 states. For B, complete all applicable steps. 1. Read the migration guide: https://open-spaced-repetition.github.io/ts-fsrs/guide/migration.md, scheduler guide: https://open-spaced-repetition.github.io/ts-fsrs/guide/scheduler/index.md, and FSRS-7 guide: https://open-spaced-repetition.github.io/ts-fsrs/guide/model/fsrs-7.md. Inspect repository instructions, pinned dependencies, all scheduling call sites, configuration, and card/log storage. Establish the actual starting point, skip completed stages, and do not downgrade existing v6/FSRS-7 code. 2. Migrate the v6 API first; only for B, upgrade the model afterward and verify each stage separately. Use defineScheduler({ model: FSRS6Model, chrono }), .use(...) and .create({ config }), preserving actual FSRS-6 weights, time policy, learning steps, fuzzing and statistics. A bare definition lacks the full old policy; add the required middleware as documented. Infer types from the new scheduler. 3. Use no legacy APIs or types in migrated code: fsrs(), FSRS, createEmptyCard, generatorParameters, old scheduler classes/strategies, or legacy Card, CardInput, ReviewLog and FSRSParameters imported from the root. Do not add a permanent compatibility layer. Keep the changes focused and preserve unrelated work. 4. Migrate cards, configuration and method calls, preserving stable cardId and application metadata and validating deserialized timestamps. Handle scheduleStatus, lastReviewAt: null for State.New, field renames, the { card, revlog } result, and the clearStatsOnForget default change explicitly. 5. Keep log timestamps distinct: v6 revlog.lastReviewAt is the pre-review previous review time, dueAt is the pre-review due date, and reviewTime is the actual current review time (not reviewAt). Ordinary v5 logs derive due from last_review || due; never universally rename it to dueAt. Without the original snapshot, do not fabricate v6 logs claiming exact rollback support. 6. Only for B: Then switch to FSRS7Model with its own 34 default weights or newly trained FSRS-7 weights and fractionalDays: true. Never reuse FSRS-6 weights or invent stabilityFast as 0.8 * stability. enableShortTerm controls explicit learning steps, not the intrinsic FSRS-7 fast trace. DefaultScheduler uses learningSteps: ['1m', '10m'], relearningSteps: ['10m'], and allowModelOverride: true by default for FSRS-7. Preserve existing learning steps explicitly instead of relying on defaults. Verify final intervals below/at least one day, disabled/empty/completed steps, and the no-middleware boundary. Import standalone curve(weights, elapsedDays, state) from ts-fsrs/models/fsrs-7; validate weights first and read retrievability, not total (the sum of mixture weights). model.forgettingCurve also returns numeric retrievability. 7. Only for B: For learned cards, select complete history since creation or the latest memory reset, map rating and actual reviewTime, and rebuild stability, stabilityFast and difficulty with Reschedule.replay(), without an old-model initialState. Apply the rebuilt memoryState only to the current card. Treat historical revlogs as read-only input; do not modify, overwrite or backfill any historical data. Only update the current card’s memory during migration; do not change when existing cards are next due for review (dueAt). FSRS-7 will calculate a new due date when the card is next reviewed. Keep lastReviewAt, cardId, learning progress and statistics unchanged. Filtering Manual does not interpret resets, and row counts or the last timestamp alone do not prove completeness. 8. Only for B: Initialize new cards with FSRS-7 memory defaults. Explicitly reject/skip learned cards with missing, truncated, empty, Manual-only or inconsistent history; retain their FSRS-6 state/config and route by model version instead of silently clearing memory. Preserve original logs, persist model version, weights/config and memory together, and establish a cross-model undo boundary. 9. If persisted data needs changing, provide backup, dry-run and recovery procedures and follow the repository's existing production workflow. Add and run focused checks for old-data conversion, all four card states, review/preview/rollback/forget, three distinct timestamps for early and overdue reviews, fractional same-day intervals, Manual/reset boundaries, missing history, and preservation of due dates, IDs, learning progress and counters. 10. Run type checks, relevant tests and the build. Report changed files, commands actually executed and their results, and any unmigrated data with reasons. Distinguish verified results from unexecuted steps. Both the migration target and the TODO List require explicit user confirmation. For other questions, inspect the repository before asking for other missing information; ask only when unresolved facts affect data correctness. ## 1. Move to the v6 API, keeping FSRS-6 ### Define the scheduling policy `defineScheduler({ model, chrono })` creates a reusable definition. `.use(...)` adds middleware to a new definition; `.create({ config })` validates configuration and creates an independent scheduler instance. Keep the definition at application scope and create instances for user or deck configuration. A bare definition does **not** include all v5 scheduling policies. This example uses the same middleware order as the current standard preset: desired retention, fuzzing, statistics, scheduled days, learning steps, maximum interval, and monotonic intervals. Preserve your actual settings and FSRS-6 weights. With `dateChrono`, `fractionalDays: false` uses UTC calendar-day differences; do not change the time policy during this API-only step without checking the effect. ```ts twoslash file="../../snippets/run-code/migration-v6.ts" fold height=520 import { dateChrono, defineScheduler, Rating, State } from 'ts-fsrs' import { schedulerDesiredRetentionMiddleware, schedulerFuzzingMiddleware, schedulerLearningStepsMiddleware, schedulerMaximumIntervalMiddleware, schedulerMonotonicIntervalMiddleware, schedulerScheduledDaysMiddleware, schedulerStatsMiddleware, } from 'ts-fsrs/middlewares' import { FSRS6Model, migrateFSRS6Parameters } from 'ts-fsrs/models/fsrs-6' // Keep the definition at application scope; create an instance per config. export const definition = defineScheduler({ model: FSRS6Model, chrono: dateChrono, }).use( schedulerDesiredRetentionMiddleware, schedulerFuzzingMiddleware, schedulerStatsMiddleware, schedulerScheduledDaysMiddleware, schedulerLearningStepsMiddleware, schedulerMaximumIntervalMiddleware, schedulerMonotonicIntervalMiddleware ) export const scheduler = definition.create({ config: { weights: migrateFSRS6Parameters(), // Pass the user's existing FSRS-6 weights here. fractionalDays: false, enableShortTerm: true, numRelearningSteps: 1, // Keep equal to relearningSteps.length. desiredRetention: 0.9, learningSteps: ['1m', '10m'], relearningSteps: ['10m'], enableFuzz: false, maximumInterval: 36500, clearStatsOnForget: false, // Match v5 forget's default counter behavior. }, }) export type Card = ReturnType export type Revlog = ReturnType['revlog'] // An application's stored v5 record, not a legacy library type. type StoredV5Card = { due: string last_review?: string | null stability: number difficulty: number state: State scheduled_days: number learning_steps: number reps: number lapses: number } export function convertCard( stored: StoredV5Card, cardId: Card['cardId'] ): Card { const scheduleStatus = { [State.New]: 'new', [State.Learning]: 'learning', [State.Review]: 'review', [State.Relearning]: 'learning', } as const return definition.schema.card.parse({ cardId, dueAt: new Date(stored.due), lastReviewAt: stored.state === State.New || stored.last_review == null ? null : new Date(stored.last_review), stability: stored.stability, difficulty: stored.difficulty, state: stored.state, scheduleStatus: scheduleStatus[stored.state], scheduledDays: stored.scheduled_days, learningStep: stored.learning_steps, reps: stored.reps, lapses: stored.lapses, }) } export const storedCard = { due: '2026-01-03T00:00:00.000Z', last_review: '2026-01-01T00:00:00.000Z', stability: 2, difficulty: 5, state: State.Review, scheduled_days: 2, learning_steps: 0, reps: 3, lapses: 0, } satisfies StoredV5Card const card = convertCard(storedCard, 'card-1') // An overdue review: previous review, original due date and now are distinct. const now = new Date('2026-01-04T00:00:00.000Z') const result = scheduler.review({ card, grade: Rating.Good, now }) const restored = scheduler.rollback({ card: result.card, revlog: result.revlog, }) console.log( JSON.stringify( { cardId: result.card.cardId, rating: result.revlog.rating, reps: result.card.reps, revlog: { lastReviewAt: result.revlog.lastReviewAt, dueAt: result.revlog.dueAt, reviewTime: result.revlog.reviewTime, }, rollbackDueAt: restored.dueAt, }, null, 2 ) ) ``` ▶ Run code The local `StoredV5Card` describes application storage; it is not imported from the legacy library. Pass an existing stable database ID as `cardId`, and keep application-specific fields separately. The scheduler's card schema validates the converted dates and fields. Validate any additional application fields at the storage boundary as well. ### Configuration mapping | v5 configuration | v6 configuration | Migration note | | ------------------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------- | | `w` | `weights` | Keep the user's FSRS-6 weights for this stage; the example uses defaults only to be runnable. | | `request_retention` | `desiredRetention` | Requires the desired-retention middleware. | | `maximum_interval` | `maximumInterval` | Requires the maximum-interval middleware; the monotonic policy also uses this limit. | | `enable_fuzz` | `enableFuzz` | Requires the fuzzing middleware; keep the old setting. | | `enable_short_term` | `enableShortTerm` | In FSRS-6, affects both model short-term behavior and explicit learning steps. | | `learning_steps` / `relearning_steps` | `learningSteps` / `relearningSteps` | Keep the configured step arrays and learning progress. | | Relearning step count | `numRelearningSteps` | For FSRS-6, set to `relearningSteps.length`. | | `forget(..., reset_count)` | `clearStatsOnForget` | Now instance configuration. v5 default: `false`; v6 stats middleware default: `true`. | ### Cards, logs and operations | Stored v5 field | v6 field or handling | | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | `due` | Card `dueAt`; deserialize to `Date`. | | `elapsed_days` / log `last_elapsed_days` | Removed. Compute elapsed time from timestamps with the selected chrono; do not use these old rounded values for FSRS-7 replay. | | `last_review` | `lastReviewAt`; missing/null becomes `null`. | | `scheduled_days` | `scheduledDays`, supplied by middleware. | | `learning_steps` | `learningStep`, supplied by middleware. | | `stability`, `difficulty`, `state`, `reps`, `lapses` | Preserve during the API migration; statistics require middleware. | | Application record ID | `cardId`, reused across the card and its logs. | | No corresponding field | `scheduleStatus`: New → `new`, Learning/Relearning → `learning`, Review → `review`. | | Review log `review` | `reviewTime`, the actual review timestamp, not the card's due date. | | Result `{ card, log }` | `{ card, revlog }`. | Derive types from your scheduler, as the example does. Card and log fields depend on the selected model, chrono and middleware. Custom v5 strategies need to be expressed through the relevant [middleware](https://open-spaced-repetition.github.io/ts-fsrs/guide/middleware.md) hooks; renaming a strategy or subclass is not sufficient. | v5 operation (reference only) | v6 operation | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `createEmptyCard(now)` | `scheduler.newCard({ now, cardId })` | | `f.next(card, now, grade)` | `scheduler.review({ card, now, grade })` | | `f.repeat(card, now)` | `scheduler.preview({ card, now })` returns an iterable with an iterator provided by `Symbol.iterator`; use `for...of` to iterate. Each candidate contains `{ grade, card, revlog }`. Select by `candidate.grade` rather than indexing a rating-keyed object. | | `f.rollback(card, log)` | `scheduler.rollback({ card, revlog })`, using the returned v6 log. | | `f.forget(card, now, reset_count)` | `scheduler.forget({ card, now })`; returns a card, not `{ card, log }`. Record manual actions in your application. | | `f.get_retrievability(card, now, false)` | For a learned card with `lastReviewAt`, call `scheduler.model.forgettingCurve(card, scheduler.chrono.difference(card.lastReviewAt, now))`; the result is numeric. Handle new cards separately and format percentages in the application. | `review()` returns new data; persist both `result.card` and `result.revlog`. New logs preserve pre-review fields needed for rollback, including `dueAt` and `lastReviewAt`. Do not merely rename an old log's `due` into a v6 rollback log: old logs may lack the required pre-review snapshot. Keep historical records unchanged; convert their rating and actual review time for model replay. Cross-version undo needs a separate application snapshot or undo boundary. Even with the same model and settings, this is not a promise of identical v5 intervals for every learning-step or fuzzing case. Check representative New, Learning, Review and Relearning cards before switching writes. [DefaultScheduler](https://open-spaced-repetition.github.io/ts-fsrs/guide/scheduler/default-scheduler.md) is an alternative preset, but its default model is FSRS-7; explicitly choose FSRS-6 if using it during this stage. ### Why the three log timestamps are not a rename For `dateChrono`, the field is **`reviewTime`, not `reviewAt`**. Consider an overdue review: the card was last reviewed on January 1, was due on January 3, and is actually reviewed on January 4. | Meaning | v5 ordinary review log | v6 `revlog` | | ------------------------------- | ---------------------------------------------------------------------- | -------------- | | Previous review time: January 1 | `due` (the old implementation writes `card.last_review \|\| card.due`) | `lastReviewAt` | | Original due date: January 3 | Not separately recorded for this learned card | `dueAt` | | Actual review time: January 4 | `review` | `reviewTime` | `lastReviewAt` is the **previous review time before the current review**. Its input and output types differ: - `DateRevlogInputFields` accepts optional `lastReviewAt?: Date | null`. If omitted or `undefined`, the schema normalizes it to `null`; `dueAt` and `reviewTime` must still be valid `Date` values. - `DateRevlogFields` always contains `lastReviewAt: Date | null`. A normal learned-card log records the pre-review `card.lastReviewAt`; the first review log for a new card with no previous review contains `null`. - `null` does not reconstruct a missing previous review time. If an old log lacks that information, passing schema validation does not guarantee exact rollback. Do not substitute `dueAt` or `reviewTime` for it. The card returned by the review has `lastReviewAt = January 4` and a newly computed `dueAt`; the log holds the **pre-review** timestamps. Rolling back with that card and its v6 log restores January 3 as `dueAt` and January 1 as `lastReviewAt`, even when the review was early or overdue. This is why chrono separates the time used to compute elapsed days from the due date saved for undo. The runnable example above prints all three values. Do not use a universal `oldLog.due → revlog.dueAt` conversion. For ordinary v5 learned-card reviews, that value normally represents the previous review. For a first review with no `last_review`, it instead contains the original due date; Manual/forget logs have their own semantics. A previously reset v5 New card can also carry an old `last_review`: the card converter normalizes `State.New` to `lastReviewAt: null` for v6. For memory replay, only map `rating` and `review → reviewTime`. For exact cross-version undo, you need the original pre-review card snapshot (including both time fields and middleware fields), or must establish an undo boundary. Neither `review` nor a recomputed interval can reliably recover a due date that was never recorded, especially after early reviews, learning steps, fuzzing or manual adjustments. ## 2. Move from FSRS-6 to FSRS-7 ### Weights and state are model-specific FSRS-6 uses 21 weights; FSRS-7 requires its own **34 weights**. Use `FSRS7_DEFAULT_WEIGHTS` or [train FSRS-7 weights](https://open-spaced-repetition.github.io/ts-fsrs/guide/optimizer.md). Do not append values to an FSRS-6 vector or reuse its trained weights. `migrateFSRS7Parameters` supplies defaults for empty input; it does not convert older parameter layouts. FSRS-7 state has `stability`, `stabilityFast` and `difficulty`. Rebuild all three from history. **Apply the rebuilt memoryState only to the current card. Historical revlogs are read-only input: do not modify, overwrite or backfill any of their data, including the memory states recorded at the time.** **Do not invent `stabilityFast = 0.8 * stability` for an existing learned card**, or retain FSRS-6 stability/difficulty while adding a fast component. The factor used by model initialization is not a migration formula for an evolved memory state. Use `fractionalDays: true` so reviews ten minutes apart retain that interval. FSRS-7 has an intrinsic fast trace: `enableShortTerm: false` only disables explicit learning steps when that middleware is installed; it does not disable the model's fast trace. There is no FSRS-7 model setting corresponding to FSRS-6's `numRelearningSteps`. ### FSRS-7 default steps and learning states For FSRS-7, `DefaultScheduler` uses the same learning steps `['1m', '10m']` and relearning steps `['10m']` as FSRS-3–6, and enables `allowModelOverride` so a longer model interval overrides the current step. Explicit settings are not overwritten by defaults. `enableShortTerm` still defaults to `true`; it controls explicit steps, not the FSRS-7 fast trace or interval-based state mapping. With `schedulerLearningStepsMiddleware` registered, the **final scheduled interval** determines the state, even if steps are disabled, empty or completed: | Pre-review state | Final interval below `graduatingInterval` (default one day) | Final interval at least `graduatingInterval` | | ------------------- | ----------------------------------------------------------- | -------------------------------------------- | | New / Learning | Learning, `scheduleStatus: learning` | Review, `scheduleStatus: review` | | Review / Relearning | Relearning, `scheduleStatus: learning` | Review, `scheduleStatus: review` | When steps are configured, a Hard, Good, or Easy rating without an applicable step is scheduled at least `graduatingInterval` later, so finishing the steps, or rating a `Review` card Hard, Good, or Easy, will always lead to being `Review`. With `allowModelOverride`, an Again that uses the model interval always leads to `Learning`/`Relearning`, even when its interval is capped at `graduatingInterval`. When no steps are configured, do not assume that no cards will be in a Learning state. only one `Again` can cause a card to become `Relearning`. Pressing Easy guarantees a card will enter a `Review` state. In general, Easy skips explicit steps, Good advances only when an applicable next step exists, and a zero-length step counts as no step. `learningStep` keeps the next step only while the card stays in Learning/Relearning; it resets to `0` when the card enters Review. It is a step index, not a review count. These transitions belong to middleware, not `FSRS7Model`. A bare scheduler combining only model and chrono still returns Review, even for a sub-day interval. Update queues, state filters and statistics that rely on the old behavior. The [complete migration example in “Replay memory and preserve the schedule”](#replay-memory-and-preserve-the-schedule) **explicitly preserves existing learning steps** instead of relying on defaults. If the product chooses fully model-driven steps, explicitly configure `learningSteps: []` and `relearningSteps: []`; this does not mean removing the learning-steps middleware. Verify that policy change separately from the card-field migration. ### Migrate retrievability calls Use `scheduler.model.forgettingCurve(state, elapsedDays)` for numeric retrievability, or `scheduler.model.algorithm.curve(elapsedDays, state)` to inspect both components. Both require the complete FSRS-7 state: `stability`, `stabilityFast` and `difficulty`, not just an FSRS-6 stability value. Alternatively, import standalone `curve` from `ts-fsrs/models/fsrs-7` and call `curve(weights, elapsedDays, state)` without constructing an algorithm instance. Supply 34 valid FSRS-7 weights. **The function does not validate weights**: validate stored or user-provided inputs through the appropriate schema/parameter checks first; a call to `curve` is not a validity test. Use the rebuilt memory state and the same FSRS-7 weights, preserving fractional `elapsedDays`. Curve results include `retrievability`, `derivative`, `fast`, `slow`, `fastWeight`, `slowWeight` and `total`. Here `total = fastWeight + slowWeight` is the sum of mixture coefficients, not the total recall probability. Read `retrievability` when replacing an old probability call, and format percentages in the application. The algorithm instance's curve result also includes `total`. These APIs calculate probabilities and components; they do not migrate cards, generate logs or schedule due dates. ### Replay memory and preserve the schedule The following example is standalone. It uses a v6-shaped FSRS-6 card, maps stored log `review` timestamps to `reviewTime`, and calls `Reschedule.replay()` without an old-model `initialState`. **Migration only rebuilds memory; it does not change when an existing card is next due for review (`dueAt`).** When the user next reviews the card, FSRS-7 calculates its new due date. The ID, last review time, learning progress and counters also stay unchanged. ```ts twoslash file="../../snippets/run-code/migration-fsrs7.ts" fold height=520 import { dateChrono, defineScheduler, Rating, State } from 'ts-fsrs' import { schedulerDesiredRetentionMiddleware, schedulerFuzzingMiddleware, schedulerLearningStepsMiddleware, schedulerMaximumIntervalMiddleware, schedulerMonotonicIntervalMiddleware, schedulerScheduledDaysMiddleware, schedulerStatsMiddleware, } from 'ts-fsrs/middlewares' import { FSRS7_DEFAULT_WEIGHTS, FSRS7Model } from 'ts-fsrs/models/fsrs-7' import { Reschedule } from 'ts-fsrs/reschedule' export const definition = defineScheduler({ model: FSRS7Model, chrono: dateChrono, }).use( schedulerDesiredRetentionMiddleware, schedulerFuzzingMiddleware, schedulerStatsMiddleware, schedulerScheduledDaysMiddleware, schedulerLearningStepsMiddleware, schedulerMaximumIntervalMiddleware, schedulerMonotonicIntervalMiddleware ) export const scheduler = definition.create({ config: { weights: FSRS7_DEFAULT_WEIGHTS, fractionalDays: true, enableShortTerm: true, // Controls learning steps, not the model's fast trace. desiredRetention: 0.9, // Preserve existing steps explicitly instead of inheriting preset defaults. learningSteps: ['1m', '10m'], relearningSteps: ['10m'], enableFuzz: false, maximumInterval: 36500, clearStatsOnForget: false, }, }) type Card = ReturnType type PreviousCard = Omit type StoredReview = { rating: Rating; review: string } // completeHistory means all reviews since creation or the latest memory reset. // Decide this from application records, not from the number of rows alone. export function migrateMemory( card: PreviousCard, logs: readonly StoredReview[], completeHistory: boolean ): Card { if (card.state === State.New) { const { stability, stabilityFast, difficulty } = scheduler.newCard({ cardId: card.cardId, }) return definition.schema.card.parse({ ...card, stability, stabilityFast, difficulty, }) } if (!completeHistory) throw new Error('Keep this card on FSRS-6: incomplete history') const history = logs .filter(({ rating }) => rating !== Rating.Manual) .map(({ rating, review }) => ({ rating, reviewTime: new Date(review), })) // replay validates dates/ratings and sorts the non-manual history. // Empty or Manual-only histories throw instead of clearing memory. const { memoryState } = new Reschedule(scheduler).replay({ history }) const latestReview = history.reduce( (latest, { reviewTime }) => Math.max(latest, reviewTime.getTime()), -Infinity ) if (card.lastReviewAt?.getTime() !== latestReview) { throw new Error( 'Keep this card on FSRS-6: history does not match lastReviewAt' ) } // Only the model memory changes; dueAt, counters and learning progress survive. return definition.schema.card.parse({ ...card, ...memoryState }) } export const previousCard: PreviousCard = { cardId: 'card-1', dueAt: new Date('2026-01-05T00:00:00.000Z'), lastReviewAt: new Date('2026-01-02T00:00:00.000Z'), stability: 2, difficulty: 5, state: State.Review, scheduleStatus: 'review', scheduledDays: 3, learningStep: 0, reps: 3, lapses: 0, } export const storedHistory: StoredReview[] = [ { rating: Rating.Good, review: '2026-01-01T00:00:00.000Z' }, { rating: Rating.Good, review: '2026-01-01T00:10:00.000Z' }, { rating: Rating.Good, review: '2026-01-02T00:00:00.000Z' }, ] const migrated = migrateMemory(previousCard, storedHistory, true) console.log( JSON.stringify( { model: 'FSRS-7', cardId: migrated.cardId, dueAt: migrated.dueAt, reps: migrated.reps, stabilityFast: migrated.stabilityFast, }, null, 2 ) ) ``` ▶ Run code | Card/history condition | Action | | ---------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | New card (`State.New`), including a deliberately reset card | Initialize FSRS-7 memory defaults; preserve its schedule and counters. | | Learned card with complete history since creation or the latest memory reset | Replay with FSRS-7 weights; persist the rebuilt state while preserving the schedule. | | Missing or truncated history | Keep the card and its matching configuration on FSRS-6. Do not silently reset it or mark history complete based on row count alone. | | Empty or Manual-only history for a learned card | Replay throws; keep the card on FSRS-6. | | Invalid dates/ratings or latest review does not match `lastReviewAt` | Stop migration for that card and reconcile its records. | `replay()` sorts valid reviews chronologically and filters `Rating.Manual`. It does not interpret manual forget/reset events. The application must select the complete history **after the latest memory reset**, excluding earlier reviews; a manual due-date adjustment alone is not a memory reset. Preserve the recorded order of equal-time reviews. With `dateChrono`, timestamps must be valid `Date` objects after deserialization. The first retained review starts from an uninitialized memory state, with zero elapsed days. The example's completeness flag is an application decision; matching the last timestamp does not prove that earlier reviews are present. Retain FSRS-6 scheduling for cards that fail migration, and route them by the persisted model version rather than passing them into FSRS-7. ### If you intentionally want to reschedule `new Reschedule(scheduler).reschedule({ history, initialCard })` replays through the whole scheduling policy and returns `{ card, results }`; it may change due dates, learning progress and statistics. Use a **new FSRS-7 card** as `initialCard`, with the existing `cardId` and the first review's time, rather than replaying full history on top of the already learned card. Its generated logs are recalculated results, not replacements for the original history. This guide defaults to `replay()` so migration does not reschedule existing cards. ## Persist and verify the migration 1. Back up cards, original logs and configuration. Store the model version with each card or a reliably associated configuration, including the exact weights used for replay. 2. Add persistence for `stabilityFast` and the v6 fields. Commit the rebuilt memory, model version and matching weight/config reference together; do not mix FSRS-6 state and FSRS-7 weights. Preserve application IDs and metadata. 3. Keep original logs unchanged. Establish an undo boundary: old-model logs must not be passed directly to FSRS-7 `rollback()`. New FSRS-7 logs must include `stabilityFast` and the pre-review fields. 4. On representative cards, verify review/preview/rollback, same-day reviews, learning steps and configured fuzzing. For memory-only migration, assert that due dates, IDs, learning progress and counters are unchanged. Resolve failures before switching those cards to FSRS-7. For the model details, see [FSRS-7](https://open-spaced-repetition.github.io/ts-fsrs/guide/model/fsrs-7.md); for the API building blocks, see [scheduler definitions](https://open-spaced-repetition.github.io/ts-fsrs/guide/scheduler.md), [Chrono](https://open-spaced-repetition.github.io/ts-fsrs/guide/chrono.md) and [middleware](https://open-spaced-repetition.github.io/ts-fsrs/guide/middleware.md). --- url: https://open-spaced-repetition.github.io/ts-fsrs/guide/architecture.md --- > For AI agents: the complete documentation index is available at https://open-spaced-repetition.github.io/ts-fsrs/llms.txt, the full documentation bundle is available at https://open-spaced-repetition.github.io/ts-fsrs/llms-full.txt. # Architecture The scheduling architecture of ts-fsrs v6 separates the memory model, time handling, and scheduling policies into `Model`, `Chrono`, and `Middleware`, with `Scheduler` coordinating their execution. ## Responsibilities | Part | Responsibility | | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | Model | Updates memory state with `step` and calculates the baseline interval with `nextInterval`; FSRS-6 tracks stability and difficulty. | | Chrono | Defines the time representation and time arithmetic. `dateChrono` uses JavaScript `Date` values and provides time differences and date arithmetic. | | Middleware | Adds scheduling policies and can extend config, card, and revlog schemas. Examples include desired retention, learning steps, and maximum interval. | | Scheduler | Combines schemas and defaults, validates configuration and inputs, and coordinates operations such as `newCard`, `review`, `preview`, and `rollback`. | Model memory state, such as stability and difficulty, is distinct from card states such as New and Learning. Scheduler and Middleware together manage card learning states and scheduling policies. Your application selects cards, stores the returned card and revlog, and manages users and collections. The scheduler does not persist results for you. ## Scheduler definitions and instances `defineScheduler({ model, chrono })` creates a definition. `.use(...)` returns a new definition with the added middleware and its inferred types; the original definition remains reusable. `.create({ config })` validates a user's configuration and creates an independent scheduler instance. :::note Reuse the complete definition Keep the definition, including middleware added with `.use(...)`, at program level. Create instances when user or collection configuration becomes available. ::: ```ts twoslash file="../../snippets/run-code/architecture.ts" import { dateChrono, defineScheduler, Rating } from 'ts-fsrs' import { schedulerDesiredRetentionMiddleware, schedulerMaximumIntervalMiddleware, } from 'ts-fsrs/middlewares' import { FSRS6_DEFAULT_WEIGHTS, FSRS6Model } from 'ts-fsrs/models/fsrs-6' // Keep the model, chronology, and policies as a reusable program preset. const schedulerDefinition = defineScheduler({ model: FSRS6Model, chrono: dateChrono, }).use(schedulerDesiredRetentionMiddleware, schedulerMaximumIntervalMiddleware) // Supply each user's configuration when creating their scheduler. const scheduler = schedulerDefinition.create({ config: { weights: FSRS6_DEFAULT_WEIGHTS, enableShortTerm: true, numRelearningSteps: 1, desiredRetention: 0.9, maximumInterval: 1, }, }) const now = new Date('2026-01-01T00:00:00.000Z') const card = scheduler.newCard({ now }) const result = scheduler.review({ card, grade: Rating.Good, now }) console.log( JSON.stringify( { originalState: card.state, state: result.card.state, dueAt: result.card.dueAt, stability: result.card.stability, rating: result.revlog.rating, }, null, 2 ) ) ``` ▶ Run code This example deliberately caps the interval at one day. The model still computes stability as `2.3065`; the maximum-interval policy changes the due date to January 2. The original card stays New, while the returned card is Review. This small preset has no learning-step middleware, statistics, or fuzzing. ## Follow a review 1. Pass `card`, `grade`, and `now` to `review`. The scheduler validates the input and uses Chrono to calculate elapsed time. 2. Middleware runs around the core operation. Desired-retention middleware supplies the retention target before the interval is calculated. 3. The core operation calculates the next memory state and calls the model for a baseline interval when needed. Middleware can supply or adjust the final scheduling interval. 4. After the chain returns, the scheduler uses Chrono to calculate `dueAt` from the review time and final interval and returns a new card and revlog. The card contains the next memory state; the revlog retains the previous memory state and this review's rating and time. ## Middleware order For `.use(A, B)`, when each review handler calls `next()`, the execution order is: `A before next() → B before next() → core → B after next() → A after next()` Code before `next()` prepares the operation; code after it sees the result of downstream handlers. Registration order therefore affects which policy has the final say. In this example, desired retention is set before the core runs, and maximum interval clamps the result afterward. When adding learning steps, register maximum-interval middleware after learning-step middleware. The learning-step middleware restores the configured learning interval on the return path, preventing day-based interval policies from overriding it. Continue with [state transitions](https://open-spaced-repetition.github.io/ts-fsrs/guide/state-transitions.md) for card lifecycle behavior, or [FSRS-6 formulas](https://open-spaced-repetition.github.io/ts-fsrs/guide/model/fsrs-6.md) for the mathematics. --- url: https://open-spaced-repetition.github.io/ts-fsrs/guide/state-transitions.md --- > For AI agents: the complete documentation index is available at https://open-spaced-repetition.github.io/ts-fsrs/llms.txt, the full documentation bundle is available at https://open-spaced-repetition.github.io/ts-fsrs/llms-full.txt. # State transitions `State` describes the card's current FSRS learning phase. A card created with `scheduler.newCard()` starts in `New`. On each review, `DefaultScheduler` combines the FSRS model result with the learning-step policy to determine the next state and due time. ![DefaultScheduler state transition flowchart](/ts-fsrs/static/svg/state-transitions.b7c406e018.svg) The diagram and table show the final scheduled interval, after explicit steps and interval middleware. Ratings affect that interval; they do not alone determine the next state. `graduatingInterval` (default `1` day) is the shortest interval for a card in `Review`. A final interval shorter than `graduatingInterval` enters or keeps `Learning`/`Relearning`; an interval of `graduatingInterval` or longer enters `Review`. This also applies when steps are disabled or empty and the model supplies the interval. An exception applies when Again uses a model interval to override an explicit step: the card stays in `Learning`/`Relearning`, even if the final interval reaches `graduatingInterval`. The diagram shows the general interval-based mapping; this exception takes precedence. Zero-length steps (such as `0m`, or durations that round to zero seconds) count as no step. Negative durations are not supported. ## What each state means | State | Meaning | Default queue | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------- | | `New` | The card is new or has been reset with `scheduler.forget()`. | `new` | | `Learning` | The card is in it's initial learning state and needs another review. It will stay in it's learning state so long as the next step is shorter than `graduatingInterval`. | `learning` | | `Review` | The card is outside short-term learning; its interval is at least `graduatingInterval`. | `review` | | `Relearning` | A previously graduated card is in a learning state again and needs another review. It will stay in a relearning state so long as it's interval is shorter than `graduatingInterval`. | `learning` | ## How ratings move a card Ratings first determine whether an explicit step supplies the interval. The table below assumes `enableShortTerm: true` and a nonempty step list for the current state: `learningSteps` for `New`/`Learning`, or `relearningSteps` for `Review`/`Relearning`. If that list is empty, the scheduler uses the model interval without a graduation minimum, even if the other list has steps. The graduation behavior below assumes `maximumInterval >= graduatingInterval` and no other middleware shortens the interval below `graduatingInterval`. | Rating | `New` / `Learning` / `Relearning` | `Review` | | ------- | --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | | `Again` | Return to the first step; reset `learningStep` to `0`. | Use the first relearning step; reset `learningStep` to `0`. | | `Hard` | Keep the current `learningStep`; use the Hard step interval. | Use the model interval, but at least `graduatingInterval`; stay in `Review`. | | `Good` | Advance when a next step exists; otherwise graduate to `Review` with the model interval, but at least `graduatingInterval`. | Use the model interval, but at least `graduatingInterval`; stay in `Review`. | | `Easy` | Skip explicit steps and graduate to `Review` with the model interval, but at least `graduatingInterval`. | Use the model interval, but at least `graduatingInterval`; stay in `Review`. | The Hard step interval is the average of the first two steps, or 1.5 times the only step. With empty steps or `enableShortTerm: false`, every rating uses the model interval, and only the state mapping below applies. `learningStep` keeps the next step only while the card stays in `Learning`/`Relearning`; it resets to `0` when the card enters `Review`. When `allowModelOverride` is enabled (the FSRS-7 default in `DefaultScheduler`), the scheduler uses the interval from the model instead of the current step if the interval from the model is longer. For Again, the model's interval is capped at `graduatingInterval`, and an Again that uses it always enters `Learning`/`Relearning`. For Hard and Good, a card whose interval reaches `graduatingInterval` graduates to `Review` without finishing the remaining steps. Interval middleware can still adjust model intervals. If `maximumInterval < graduatingInterval`, the cap can prevent graduation. For example, with `graduatingInterval: 2` and `maximumInterval: 1`, finishing the learning steps produces a model-based interval capped at 1 day, so the card stays in `Learning`. After the interval is resolved, `schedulerLearningStepsMiddleware` uses the table below to determine the next state. The Again override exception described above takes precedence. The table shows state transitions, not how each rating selects an interval: | Current state | Final interval \< `graduatingInterval` | Final interval ≥ `graduatingInterval` | | ------------- | -------------------------------------- | ------------------------------------- | | `New` | `Learning` | `Review` | | `Learning` | `Learning` | `Review` | | `Review` | `Relearning` | `Review` | | `Relearning` | `Relearning` | `Review` | When an explicit step applies, the scheduler keeps its configured duration unless the model from `allowModelOverride` provides a longer interval. A step of `graduatingInterval` or longer still moves the card to `Review`. :::warning Learning states require the `schedulerLearningStepsMiddleware` middleware `DefaultScheduler` registers a `schedulerLearningStepsMiddleware` and defaults `enableShortTerm` to `true` including for FSRS-7. For FSRS-7, this flag controls explicit `learningSteps` and `relearningSteps`, not its intrinsic fast trace or the interval-based state mapping. With `enableShortTerm: false`, an interval below `graduatingInterval` still enters the `Learning` or `Relearning` phase. If you create a custom scheduler with `defineScheduler`, register the middleware with `.use(schedulerLearningStepsMiddleware)` to get this state mapping; without it, the default behaviour remains `Review` with `scheduleStatus: review`, even when FSRS-7 returns an interval below one day. The model computes memory states and intervals; learning-state transitions are middleware behavior. To produce `Learning`/`Relearning` states after FSRS-7 scheduling, register `schedulerLearningStepsMiddleware` or implement your own state-transition middleware. ::: `Rating.Manual` is not a review grade. Pass one of `Again`, `Hard`, `Good`, or `Easy` to `review()`. ## Run the transitions The runnable example below uses the default FSRS-7 model with this configuration: ```ts const scheduler = await DefaultScheduler({ learningSteps: ['1m', '10m'], relearningSteps: ['10m'], enableFuzz: false, allowModelOverride: false, }) ``` `graduatingInterval` uses its default of **1 day**. The card starts in `New` with `learningStep: 0`. `learningStep` is a zero-based step index, not a review count; the same field tracks progress through either the learning or relearning steps. With `allowModelOverride: false`, the example produces these transitions: | Current state | Current learningStep | Rating | Resulting state | Resulting learningStep | Interval | | ------------- | -------------------- | ------ | --------------- | ---------------------- | ----------- | | New | 0 | Again | Learning | 0 | 1 minute | | Learning | 0 | Good | Learning | 1 | 10 minutes | | Learning | 1 | Hard | Learning | 1 | 5.5 minutes | | Learning | 1 | Again | Learning | 0 | 1 minute | | Learning | 1 | Good | Review | 0 | 1 day | | Review | 0 | Again | Relearning | 0 | 10 minutes | | Relearning | 0 | Good | Review | 0 | 1 day | The Hard, Again, and Good rows at `learningStep: 1` are alternative ratings of the same card, not consecutive reviews. The relearning branch starts from a separate card that reached `Review` via Easy. Disabling `allowModelOverride` keeps the configured step intervals. If enabled, a longer model interval can replace an explicit step. For example, a hypothetical 30-minute model interval would extend the 10-minute Good step to 30 minutes. A shorter model interval would leave that step unchanged. The scheduler still uses model intervals after the steps finish or when no step applies, even with `allowModelOverride: false`. The full example below uses the public `DefaultSchedulerCard`, `Grade`, and `State` types. Its output shows intervals in minutes rounded to two decimals. :::note dueAt is not now `dueAt` is when the card becomes due, while `now` is the actual review time and should be equal to or later than `dueAt`. To keep the example deterministic without conflating them, the helper reviews each card 30 seconds after `dueAt`. ::: ```ts twoslash file="../../snippets/run-code/state-transitions.ts" import { DefaultScheduler, type DefaultSchedulerCard, type Grade, Rating, State, } from 'ts-fsrs' const scheduler = await DefaultScheduler({ learningSteps: ['1m', '10m'], relearningSteps: ['10m'], enableFuzz: false, allowModelOverride: false, }) const stateNames: Record = { [State.New]: 'New', [State.Learning]: 'Learning', [State.Review]: 'Review', [State.Relearning]: 'Relearning', } const reviewAfterDue = (card: DefaultSchedulerCard, grade: Grade) => { const reviewedAt = new Date(card.dueAt.getTime() + 30_000) return scheduler.review({ card, grade, now: reviewedAt }).card } const newCard = scheduler.newCard({ cardId: 'state-transitions', now: new Date('2026-01-01T00:00:00.000Z'), }) const learning = reviewAfterDue(newCard, Rating.Again) const nextLearningStep = reviewAfterDue(learning, Rating.Good) const afterLearningSteps = reviewAfterDue(nextLearningStep, Rating.Good) const repeated = reviewAfterDue(nextLearningStep, Rating.Hard) const restarted = reviewAfterDue(nextLearningStep, Rating.Again) const reviewed = reviewAfterDue(newCard, Rating.Easy) const relearning = reviewAfterDue(reviewed, Rating.Again) const afterRelearningSteps = reviewAfterDue(relearning, Rating.Good) for (const [from, rating, to] of [ [newCard, 'Again', learning], [learning, 'Good', nextLearningStep], [nextLearningStep, 'Hard', repeated], [nextLearningStep, 'Again', restarted], [nextLearningStep, 'Good', afterLearningSteps], [newCard, 'Easy', reviewed], [reviewed, 'Again', relearning], [relearning, 'Good', afterRelearningSteps], ] as const) { console.log( `${stateNames[from.state]} --${rating}--> ${stateNames[to.state]}; ` + `learningStep: ${from.learningStep} -> ${to.learningStep}; ` + `interval: ${(to.scheduledDays * 1440).toFixed(2)}m` ) } ``` ▶ Run code ## Card, revlog, and memory state - The input `card` is the state before a review; `review()` does not mutate it. - `result.card` contains the next `state`, next FSRS memory state (`stability`, `stabilityFast`, and `difficulty` for FSRS-7), and next `dueAt`. - `result.revlog` records the previous state and memory state together with the selected rating and review timing. That snapshot is also what rollback uses to restore the card. - `scheduleStatus` controls queue membership. By default it maps `New` to `new`, both learning states to `learning`, and `Review` to `review`; middleware may add statuses such as `suspended` without changing the four FSRS states. Use `card.state` to find the learning phase, and `card.scheduleStatus` to filter scheduling queues. --- url: https://open-spaced-repetition.github.io/ts-fsrs/guide/llms.md --- > For AI agents: the complete documentation index is available at https://open-spaced-repetition.github.io/ts-fsrs/llms.txt, the full documentation bundle is available at https://open-spaced-repetition.github.io/ts-fsrs/llms-full.txt. # LLM Every page on this site is also published as Markdown, and the updates section is published as an Atom feed, so agents and feed readers never have to parse the rendered HTML. For your Agent Answer ts-fsrs questions from the documentation Copy Prompt Use the ts-fsrs documentation as your source of truth. 1. Fetch the documentation index at https://open-spaced-repetition.github.io/ts-fsrs/llms.txt. 2. Follow the links that cover my question and read their Markdown. 3. Answer using the current ts-fsrs public API, and cite the pages you used. ## Documentation as Markdown [`/ts-fsrs/llms.txt`](/ts-fsrs/llms.txt) indexes every page, grouped by top navigation, with one description per link. [`/ts-fsrs/llms-full.txt`](/ts-fsrs/llms-full.txt) concatenates the Markdown of every page into a single file. [`/ts-fsrs/guide/index.md`](/ts-fsrs/guide/index.md) is the Markdown behind one page. Append `.md` to any route to get its source. Start from `llms.txt` when the agent can follow links on demand; reach for `llms-full.txt` when it needs the whole site in one request. Each generated Markdown file opens with a hint line pointing back at these entry points, so an agent that lands on a single page can still discover the rest. Every language has its own copy of these files, written in that language. Switch languages to get them. ## Feeds Updates are published in English through a single Atom feed: [`/ts-fsrs/rss/updates.xml`](/ts-fsrs/rss/updates.xml). Update pages also declare this feed in the document head for readers that support auto-discovery. --- url: https://open-spaced-repetition.github.io/ts-fsrs/guide/model/index.md --- > For AI agents: the complete documentation index is available at https://open-spaced-repetition.github.io/ts-fsrs/llms.txt, the full documentation bundle is available at https://open-spaced-repetition.github.io/ts-fsrs/llms-full.txt. # Model overview An FSRS model turns a rating and elapsed time into a new memory state, estimates recall probability with a forgetting curve, and converts memory state plus desired retention into a base interval. The FSRS-3 through FSRS-6 terminology and equations in this section are reviewed against the [awesome-fsrs Wiki](https://github.com/open-spaced-repetition/awesome-fsrs/wiki), especially [The Algorithm](https://github.com/open-spaced-repetition/awesome-fsrs/wiki/The-Algorithm). The TypeScript APIs and framework boundaries describe ts-fsrs itself. :::note Model boundary A model owns memory-state updates, forgetting-curve evaluation, and base-interval calculation. The scheduler orchestrates reviews, `chrono` handles dates and elapsed time, and middleware applies learning steps, fuzzing, maximum-interval limits, and queue-status policies. ::: For your Agent Implement or explain an SRS model Copy Prompt Use the current ts-fsrs Model contract for this task: [describe the task]. 1. Read https://open-spaced-repetition.github.io/ts-fsrs/guide/model/index.md and the page for the selected model version. 2. When useful, compare the formulas and descriptions for FSRS-6 (https://open-spaced-repetition.github.io/ts-fsrs/guide/model/fsrs-6.md), FSRS-5 (https://open-spaced-repetition.github.io/ts-fsrs/guide/model/fsrs-5.md), FSRS-4.5 (https://open-spaced-repetition.github.io/ts-fsrs/guide/model/fsrs-4dot5.md), FSRS-4 (https://open-spaced-repetition.github.io/ts-fsrs/guide/model/fsrs-4.md), and FSRS-3 (https://open-spaced-repetition.github.io/ts-fsrs/guide/model/fsrs-3.md). FSRS-7: https://open-spaced-repetition.github.io/ts-fsrs/guide/model/fsrs-7.md. 3. Define the config and memory-state schemas before implementing the model. 4. Keep the model limited to step, nextInterval, forgettingCurve, and forward behavior. 5. Pass desired retention explicitly to nextInterval. 6. Let the scheduler orchestrate reviews, chrono handle time, and middleware apply learning steps, fuzzing, maximum-interval limits, and queue-status policies. 7. Use bypass only after the config has already been migrated, clipped, and checked. 8. Use the current public API and add focused tests for every implemented equation. ## Shared symbols | Symbol | Meaning | | -------------------------------- | -------------------------------------------------------------------------------------------------------- | | `math:D` | Difficulty from 1 (easiest) to 10 (hardest). | | `math:S` | FSRS-3–6 stability in days; recall probability is 90% when `math:t = S`. | | `math:R` | Estimated probability of recall after `math:t` days. | | `math:G` | Grade: 1 Again, 2 Hard, 3 Good, or 4 Easy. | | `math:t` | Elapsed days since the previous review. | | `math:r` | Desired retention used by `nextInterval`. | | `math:I(r, S)` | Base interval in days for stability `math:S` and desired retention `math:r`, before middleware policies. | | `math:S_{\min}`, `math:S_{\max}` | Lower and upper stability bounds of the model. | | `math:D_{\min}`, `math:D_{\max}` | Lower and upper difficulty bounds of the model. | | `math:w_i` | A trained model weight. | [FSRS-7](https://open-spaced-repetition.github.io/ts-fsrs/guide/model/fsrs-7.md) is documented separately from its implementation snapshot: it adds a fast stability, uses a mixed curve, and returns fractional-day intervals. The 90% definition of stability and `I(r, S)` notation above apply to FSRS-3–6. ## Use a versioned model `DefaultScheduler` defaults to FSRS-7 and can select any supported FSRS version through its `version` option. Every versioned model can also be created and called directly; the following example uses FSRS-6: ```ts twoslash file="../../../snippets/run-code/model.ts" import { Rating } from 'ts-fsrs' import { FSRS6_DEFAULT_WEIGHTS, FSRS6Model } from 'ts-fsrs/models/fsrs-6' const model = FSRS6Model.create({ config: { weights: FSRS6_DEFAULT_WEIGHTS, enableShortTerm: true, numRelearningSteps: 1, // Constrains weights; middleware runs the steps. }, }) const memoryState = model.step({ memoryState: null, rating: Rating.Good, elapsedDays: 0, }) const desiredRetention = 0.9 const interval = model.nextInterval(memoryState, desiredRetention) const retrievabilityAtInterval = model.forgettingCurve(memoryState, interval) console.log( JSON.stringify( { memoryState, desiredRetention, interval, retrievabilityAtInterval, }, null, 2 ) ) ``` ▶ Run code The example first calculates an interval for 90% desired retention, then uses `forgettingCurve` to estimate the recall probability at that interval. The result may differ slightly from 90% because the FSRS-6 `nextInterval` returns whole days. In the FSRS-6 config, `enableShortTerm` controls the model's same-day stability update. `numRelearningSteps` only informs weight clipping and checking; relearning-step scheduling remains middleware behavior. ## Define a model `defineModel` preserves the concrete config, memory-state, and algorithm types while packaging schemas, defaults, and a model-core factory into one definition. It does not parse config by itself: The following example implements an [SM-2 variant](https://github.com/open-spaced-repetition/srs-benchmark/blob/main/models/sm2_trainable.py) to demonstrate how to define a model. ```ts twoslash fold height=420 file="../../../snippets/run-code/custom-model.ts" import { clamp, defineModel, type ModelForwardInput, type ModelStepInput, Rating, } from 'ts-fsrs' import { z } from 'zod' const configSchema = z.object({ weights: z.tuple([ z.number().min(0.01).max(100), z.number().min(0.01).max(100), z.number().min(1.3).max(10), z.number().nonnegative(), z.number().min(5), z.number(), ]), }) const memoryStateSchema = z.object({ interval: z.number().min(0).max(36500), easeFactor: z.number().min(1.3).max(10), repetitions: z.number().int().nonnegative(), }) type MemoryState = z.output const SM2Model = defineModel({ name: 'sm2', schema: { config: configSchema, memoryState: memoryStateSchema }, defaultValue: { memoryState: ({ config }) => ({ interval: 0, easeFactor: config.weights[2], repetitions: 0, }), }, create({ config }) { const parsed = configSchema.parse(config) const w = parsed.weights const step = ({ memoryState, rating }: ModelStepInput) => { const previous = memoryState ?? { interval: 0, easeFactor: parsed.weights[2], repetitions: 0, } // Again / Hard / Good / Easy -> SM-2 quality 2 / 3 / 4 / 5. const quality = rating + 1 const repetitions = rating > Rating.Again ? previous.repetitions + 1 : 0 const interval = clamp( repetitions <= 1 ? w[0] : repetitions === 2 ? w[1] : previous.interval * previous.easeFactor, 0.01, 36500 ) const easeFactor = clamp( previous.easeFactor - w[3] * (quality - w[4]) ** 2 + w[5], 1.3, 10 ) return { interval, easeFactor, repetitions } } const forward = ({ history, initialState, }: ModelForwardInput) => { const states: MemoryState[] = [] let memoryState = initialState ?? null for (const review of history) { memoryState = step({ memoryState, rating: review.rating, elapsedDays: review.deltaT, }) states.push(memoryState) } return states } return { config: parsed, bounds: { iMin: 0.01, iMax: 36500, eMin: 1.3, eMax: 10, rMin: 0, rMax: Infinity, }, algorithm: null, step, // Invert the benchmark curve to satisfy the Model interval contract. nextInterval: (state: Readonly, desiredRetention: number) => Math.max( 1, Math.round( (state.interval * Math.log(desiredRetention)) / Math.log(0.9) ) ), forgettingCurve: (state: Readonly, elapsedDays: number) => 0.9 ** (elapsedDays / state.interval), forward, } }, }) const model = SM2Model.create({ config: { weights: [1, 6, 2.5, 0.02, 7, 0.18] }, }) const states = model.forward({ history: [ { rating: Rating.Good, deltaT: 0 }, { rating: Rating.Good, deltaT: 1 }, { rating: Rating.Good, deltaT: 6 }, { rating: Rating.Again, deltaT: 15 }, { rating: Rating.Good, deltaT: 1 }, { rating: Rating.Good, deltaT: 1 }, ], }) console.log(JSON.stringify(states, null, 2)) // Intervals: 1, 6, 15, 1, 1, 6 days. ``` ▶ Run code The schemas describe the public boundary and are consumed when a scheduler creates its schema. A direct `create` implementation must perform any required parsing and normalization before returning `config`, `bounds`, `algorithm`, `step`, `nextInterval`, `forgettingCurve`, and `forward`. Keep that core deterministic and free of scheduler policy. :::warning Use bypass only with prepared config Only use `FSRS6Model.create({ config, bypass: true })` when the caller has explicitly completed that model's migration, clipping, schema validation, and range checks. It skips those create-time steps and freezing. Never use `bypass` for raw user, storage, or network input. ::: ## Model versions - [FSRS-7](https://open-spaced-repetition.github.io/ts-fsrs/guide/model/fsrs-7.md) — dual-trace implementation reference; 34 weights and numerical intervals. - [FSRS-6](https://open-spaced-repetition.github.io/ts-fsrs/guide/model/fsrs-6.md) — trainable decay and same-day updates. - [FSRS-5](https://open-spaced-repetition.github.io/ts-fsrs/guide/model/fsrs-5.md) — fixed power forgetting curve with same-day updates. - [FSRS-4.5](https://open-spaced-repetition.github.io/ts-fsrs/guide/model/fsrs-4dot5.md) — fixed power curve without a same-day formula. - [FSRS-4](https://open-spaced-repetition.github.io/ts-fsrs/guide/model/fsrs-4.md) — the first power-curve version in this package. - [FSRS-3](https://open-spaced-repetition.github.io/ts-fsrs/guide/model/fsrs-3.md) — the earlier exponential forgetting curve. --- url: https://open-spaced-repetition.github.io/ts-fsrs/guide/model/fsrs-7.md --- > For AI agents: the complete documentation index is available at https://open-spaced-repetition.github.io/ts-fsrs/llms.txt, the full documentation bundle is available at https://open-spaced-repetition.github.io/ts-fsrs/llms-full.txt. # FSRS-7 :::tip Migration guide For steps that rebuild memory states without rescheduling existing cards, see the [v6 and FSRS-7 migration guide](https://open-spaced-repetition.github.io/ts-fsrs/guide/migration.md). ::: FSRS-7 uses 34 weights and two stability components. Its state contains `stability` (slow component), `stabilityFast` (fast component), and `difficulty`. The two stability components produce the slow and fast recall components used by the forgetting curve. ## Memory state and notation Use the [shared symbols](https://open-spaced-repetition.github.io/ts-fsrs/guide/model.md#shared-symbols), with `math:S_s` for `stability` and `math:S_f` for `stabilityFast`. Both are measured in days, but neither is the interval at which the mixed recall probability reaches 90%. Difficulty affects both the slow curve and its mixture weight. The formulas use bounded pre-review state values: both stabilities are in `[0.0001, 36500]` and difficulty is in `[1, 10]`. Elapsed time `math:t` is in fractional days; curve evaluation uses `math:\max(t,0)`. The fast and slow names identify model components, not a guaranteed ordering of their stability values. ## Initial memory For a new memory state, let `math:C_s` and `math:C_f` clamp to the slow and fast stability bounds respectively, and let `math:C_D` clamp to `[1, 10]`. ```math S_{s,0}=C_s(w_{G-1}),\qquad S_{f,0}=C_f(0.8S_{s,0}) ``` ```math D_0(G)=w_4-e^{w_5(G-1)}+1,\qquad D_{initial}=C_D(D_0(G)) ``` ## Dual-trace forgetting curve Fast trace: ```math d_f=-\operatorname{clamp}(w_{23}S_f^{w_{33}-0.3},0.01,0.95) ``` ```math F_f=\exp\left(\min\left(\frac{\ln w_{25}}{d_f},60\right)\right)-1 ``` ```math R_f(t)=\left(1+F_f\frac{t}{S_f}\right)^{d_f} ``` Slow trace: ```math d_s=-\operatorname{clamp}(w_{24},0.01,0.95) ``` ```math F_s=(w_{26}^{1/d_s}-1)e^{(D-5)(w_{32}-0.3)} ``` ```math R_s(t)=\left(1+F_s\frac{t}{S_s}\right)^{d_s} ``` ## Mixture weights and recall The weights are positive, unnormalised coefficients. They depend on the memory state, not elapsed time. A raw component recall is not its contribution to the mixture. ```math a_f=w_{27}S_f^{-w_{29}},\qquad a_s=w_{28}S_s^{w_{30}}e^{(D-5)(w_{31}-0.5)} ``` ```math R(t)=\frac{a_fR_f(t)+a_sR_s(t)}{a_f+a_s}(1-2\varepsilon)+\varepsilon,\qquad\varepsilon=10^{-5} ``` `algorithm.curve(t, state)` returns `fast`, `slow`, `fastWeight`, `slowWeight`, `retrievability`, and `derivative`. The `fast` and `slow` fields are the two component recall probabilities, while `fastWeight` and `slowWeight` are positive, unnormalised mixture coefficients. The `retrievability` value is their weighted average followed by the final epsilon adjustment, so `math:R(0)=0.99999`. ## Stability after review For a trace stability `math:x`, recall input `math:q`, and parameter offset `math:k`, define the lapse value and successful-recall update: ```math L_k(x,q)=\min\left(x,w_{k+3}((x+1)^{w_{k+4}}-1)e^{(1-q)w_{k+5}}\right) ``` ```math H_{k,G}=\begin{cases}w_{k+6},&G=2\\1,&G\ne2\end{cases},\qquad E_{k,G}=\begin{cases}w_{k+7},&G=4\\1,&G\ne4\end{cases} ``` ```math U_k(x,q,G)=x\left[1+e^{w_k-1.5}(11-D)x^{-w_{k+1}}(e^{(1-q)w_{k+2}}-1)H_{k,G}E_{k,G}\right] ``` ```math T_k(x,q,G)=\begin{cases}L_k(x,q),&G=1\\\max(L_k(x,q),U_k(x,q,G)),&G\in\{2,3,4\}\end{cases} ``` Use `math:k=7` and the mixed recall `math:R` for the slow trace; use `math:k=15` and the raw fast recall `math:R_f` for the fast trace. All updates use pre-review difficulty. An explicit `retrievability` input replaces `math:R` for the slow update and difficulty, but does not replace `math:R_f` for the fast update. On Again, cap the new fast stability at 80% of the new slow stability before applying state bounds: ```math \widetilde S_s=T_7(S_s,R,G),\qquad \widetilde S_f=T_{15}(S_f,R_f,G) ``` ```math S'_s=C_s(\widetilde S_s),\qquad S'_f=C_f\left(\begin{cases}\min(\widetilde S_f,0.8\widetilde S_s),&G=1\\\widetilde S_f,&G\ge2\end{cases}\right) ``` ## Difficulty after review Again scales the difficulty change by the mixed recall. Mean reversion uses a fixed coefficient of `0.01`: ```math \Delta D=-w_6(G-3)\begin{cases}R+0.1,&G=1\\1,&G\ge2\end{cases} ``` ```math D'=C_D\left(0.01D_0(4)+0.99\left(D+\frac{10-D}{9}\Delta D\right)\right) ``` The implementation rounds the three new state values to eight decimal places and clamps them to their bounds. FSRS-7 uses these updates at fractional elapsed times, including zero; it has no separate `enableShortTerm` same-day formula. Learning-step scheduling is still a middleware policy. ## Interval calculation `nextInterval(state, r)` numerically solves the mixed curve for elapsed time. It depends on all three state fields and returns fractional days. ```math R(I;S_s,S_f,D)=r ``` The implementation validates `math:0 For AI agents: the complete documentation index is available at https://open-spaced-repetition.github.io/ts-fsrs/llms.txt, the full documentation bundle is available at https://open-spaced-repetition.github.io/ts-fsrs/llms-full.txt. # FSRS-6 FSRS-6 has 21 trainable weights, including a trainable forgetting-curve decay and a same-day stability term. See the [Model overview](https://open-spaced-repetition.github.io/ts-fsrs/guide/model.md) for the shared symbols and Model contract. :::info Formula source Reviewed against [awesome-fsrs Wiki — FSRS-6](https://github.com/open-spaced-repetition/awesome-fsrs/wiki/The-Algorithm#fsrs-6). ::: ## Forgetting curve and interval With the positive decay magnitude `math:d = w_{20}`, choose `math:F` so that `math:R(S, S) = 0.9`: ```math F=0.9^{-1/d}-1,\qquad R(t,S)=\left(1+F\frac{t}{S}\right)^{-d} ``` ```math I(r,S)=\frac{S}{F}\left(r^{-1/d}-1\right) ``` `nextInterval` rounds the result to whole days and returns at least one day. ## Initial memory ```math S_0(G)=\operatorname{clamp}\left(w_{G-1},S_{min},S_{max}\right) ``` ```math D_0(G)=w_4-e^{w_5(G-1)}+1,\qquad D_{initial}=\operatorname{clamp}\left(D_0(G),1,10\right) ``` ## Difficulty ```math \Delta D=-w_6(G-3),\qquad D^*=D+\Delta D\frac{10-D}{9} ``` ```math D'=\operatorname{clamp}\left(w_7D_0(4)+(1-w_7)D^*,1,10\right) ``` ## Stability after review For `math:t > 0`, or when `enableShortTerm` is disabled, the rating selects the following branch. The update uses the pre-review `math:D` and `math:S`. `math:H_G = w_{15}` for Hard and `math:1` otherwise; `math:E_G = w_{16}` for Easy and `math:1` otherwise. When `enableShortTerm` is enabled, `math:C` is the previous stability multiplied by `math:\exp(-w_{17}w_{18})`; otherwise, `math:C = S`: ```math S_f=\min\left(w_{11}D^{-w_{12}}\left((S+1)^{w_{13}}-1\right)e^{w_{14}(1-R)},C\right) ``` ```math S_r=S\left[1+e^{w_8}(11-D)S^{-w_9}\left(e^{w_{10}(1-R)}-1\right)H_GE_G\right] ``` ```math S'_{raw}(G)= \begin{cases} S_f,&G=1\\ S_r,&G\in\{2,3,4\} \end{cases} ``` `math:C` is an implementation cap for post-lapse stability, not a separately configured stability floor. The model then clamps `math:S'_{\mathrm{raw}}` to the range from `math:S_{\min}` to `math:S_{\max}`. ## Same-day update Here, “same-day” means the model receives `elapsedDays = 0`; the model does not inspect calendar dates. Direct callers and the scheduler’s `chrono` determine the elapsed value. When `enableShortTerm` is enabled and `math:t = 0`: ```math s_{inc}=S^{-w_{19}}e^{w_{17}(G-3+w_{18})} ``` ```math S'=\operatorname{clamp}\left(Sm,S_{min},S_{max}\right), \qquad m=\begin{cases} s_{inc},&G=1\\ \max(s_{inc},1),&G\ge2 \end{cases} ``` Hard, Good, and Easy cannot reduce stability below its current value; Again may reduce it. --- url: https://open-spaced-repetition.github.io/ts-fsrs/guide/model/fsrs-5.md --- > For AI agents: the complete documentation index is available at https://open-spaced-repetition.github.io/ts-fsrs/llms.txt, the full documentation bundle is available at https://open-spaced-repetition.github.io/ts-fsrs/llms-full.txt. # FSRS-5 FSRS-5 has 19 trainable weights. It keeps the fixed FSRS-4.5 forgetting curve and adds a grade-dependent same-day stability update. See the [Model overview](https://open-spaced-repetition.github.io/ts-fsrs/guide/model.md) for the shared symbols and Model contract. :::info Formula source Reviewed against [awesome-fsrs Wiki — FSRS-5](https://github.com/open-spaced-repetition/awesome-fsrs/wiki/The-Algorithm#fsrs-5). ::: ## Forgetting curve and interval ```math R(t,S)=\left(1+\frac{19t}{81S}\right)^{-1/2} ``` ```math I(r,S)=\frac{81S}{19}\left(r^{-2}-1\right) ``` ## Initial memory ```math S_0(G)=\operatorname{clamp}\left(w_{G-1},S_{min},S_{max}\right) ``` ```math D_0(G)=w_4-e^{w_5(G-1)}+1,\qquad D_{initial}=\operatorname{clamp}\left(D_0(G),1,10\right) ``` ## Difficulty ```math \Delta D=-w_6(G-3),\qquad D^*=D+\Delta D\frac{10-D}{9} ``` ```math D'=\operatorname{clamp}\left(w_7D_0(4)+(1-w_7)D^*,1,10\right) ``` ## Stability after review For `math:t > 0`, or when `enableShortTerm` is disabled, the rating selects the following branch. `math:H_G = w_{15}` for Hard and `math:1` otherwise; `math:E_G = w_{16}` for Easy and `math:1` otherwise. When `enableShortTerm` is enabled, `math:C` is the previous stability multiplied by `math:\exp(-w_{17}w_{18})`; otherwise, `math:C = S`: ```math S_f=\min\left(w_{11}D^{-w_{12}}\left((S+1)^{w_{13}}-1\right)e^{w_{14}(1-R)},C\right) ``` ```math S_r=S\left[1+e^{w_8}(11-D)S^{-w_9}\left(e^{w_{10}(1-R)}-1\right)H_GE_G\right] ``` ```math S'_{raw}(G)= \begin{cases} S_f,&G=1\\ S_r,&G\in\{2,3,4\} \end{cases} ``` `math:C` is an implementation cap for post-lapse stability, not a separately configured stability floor. The model then clamps `math:S'_{\mathrm{raw}}` to the range from `math:S_{\min}` to `math:S_{\max}`. ## Same-day update Here, “same-day” means the model receives `elapsedDays = 0`; the model does not inspect calendar dates. Direct callers and the scheduler’s `chrono` determine the elapsed value. When `enableShortTerm` is enabled and `math:t = 0`: ```math S'=\operatorname{clamp}\left( S e^{w_{17}(G-3+w_{18})},S_{min},S_{max} \right) ``` --- url: https://open-spaced-repetition.github.io/ts-fsrs/guide/model/fsrs-4dot5.md --- > For AI agents: the complete documentation index is available at https://open-spaced-repetition.github.io/ts-fsrs/llms.txt, the full documentation bundle is available at https://open-spaced-repetition.github.io/ts-fsrs/llms-full.txt. # FSRS-4.5 FSRS-4.5 has 17 trainable weights. It changes the forgetting curve from FSRS-4 while retaining the same difficulty and successful-recall structure. See the [Model overview](https://open-spaced-repetition.github.io/ts-fsrs/guide/model.md) for the shared symbols and Model contract. :::info Formula source Reviewed against [awesome-fsrs Wiki — FSRS-4.5](https://github.com/open-spaced-repetition/awesome-fsrs/wiki/The-Algorithm#fsrs-45). ::: ## Forgetting curve and interval ```math R(t,S)=\left(1+\frac{19t}{81S}\right)^{-1/2} ``` ```math I(r,S)=\frac{81S}{19}\left(r^{-2}-1\right) ``` ## Initial memory ```math S_0(G)=\operatorname{clamp}\left(w_{G-1},S_{min},S_{max}\right) ``` ```math D_0(G)=\operatorname{clamp}\left(w_4-(G-3)w_5,D_{min},D_{max}\right) ``` ## Difficulty ```math D^*=D-w_6(G-3) ``` ```math D'=\operatorname{clamp}\left(w_7w_4+(1-w_7)D^*,D_{min},D_{max}\right) ``` ## Stability after review `math:H_G = w_{15}` for Hard and `math:1` otherwise; `math:E_G = w_{16}` for Easy and `math:1` otherwise. ```math S_f=\min\left(w_{11}D^{-w_{12}}\left((S+1)^{w_{13}}-1\right)e^{w_{14}(1-R)},S\right) ``` ```math S_r=S\left[1+e^{w_8}(11-D)S^{-w_9}\left(e^{w_{10}(1-R)}-1\right)H_GE_G\right] ``` ```math S'_{raw}(G)= \begin{cases} S_f,&G=1\\ S_r,&G\in\{2,3,4\} \end{cases} ``` The Again branch prevents post-lapse stability from exceeding the previous `math:S`. The model then clamps `math:S'_{\mathrm{raw}}` to the range from `math:S_{\min}` to `math:S_{\max}`. --- url: https://open-spaced-repetition.github.io/ts-fsrs/guide/model/fsrs-4.md --- > For AI agents: the complete documentation index is available at https://open-spaced-repetition.github.io/ts-fsrs/llms.txt, the full documentation bundle is available at https://open-spaced-repetition.github.io/ts-fsrs/llms-full.txt. # FSRS-4 FSRS-4 has 17 trainable weights and replaces the FSRS-3 exponential curve with a power forgetting curve. See the [Model overview](https://open-spaced-repetition.github.io/ts-fsrs/guide/model.md) for the shared symbols and Model contract. :::info Formula source Reviewed against [awesome-fsrs Wiki — FSRS-4](https://github.com/open-spaced-repetition/awesome-fsrs/wiki/The-Algorithm#fsrs-v4). ::: ## Forgetting curve and interval ```math R(t,S)=\left(1+\frac{t}{9S}\right)^{-1} ``` ```math I(r,S)=9S\left(\frac{1}{r}-1\right) ``` ## Initial memory ```math S_0(G)=\operatorname{clamp}\left(w_{G-1},S_{min},S_{max}\right) ``` ```math D_0(G)=\operatorname{clamp}\left(w_4-(G-3)w_5,D_{min},D_{max}\right) ``` ## Difficulty ```math D^*=D-w_6(G-3) ``` ```math D'=\operatorname{clamp}\left(w_7w_4+(1-w_7)D^*,D_{min},D_{max}\right) ``` ## Stability after review `math:H_G = w_{15}` for Hard and `math:1` otherwise; `math:E_G = w_{16}` for Easy and `math:1` otherwise. ```math S_f=w_{11}D^{-w_{12}}\left((S+1)^{w_{13}}-1\right)e^{w_{14}(1-R)} ``` ```math S_r=S\left[1+e^{w_8}(11-D)S^{-w_9}\left(e^{w_{10}(1-R)}-1\right)H_GE_G\right] ``` ```math S'_{raw}(G)= \begin{cases} S_f,&G=1\\ S_r,&G\in\{2,3,4\} \end{cases} ``` The model then clamps `math:S'_{\mathrm{raw}}` to the range from `math:S_{\min}` to `math:S_{\max}`. --- url: https://open-spaced-repetition.github.io/ts-fsrs/guide/model/fsrs-3.md --- > For AI agents: the complete documentation index is available at https://open-spaced-repetition.github.io/ts-fsrs/llms.txt, the full documentation bundle is available at https://open-spaced-repetition.github.io/ts-fsrs/llms-full.txt. # FSRS-3 FSRS-3 has 13 trainable weights and uses an exponential forgetting curve. Its stability updates use the newly calculated difficulty `math:D'`. See the [Model overview](https://open-spaced-repetition.github.io/ts-fsrs/guide/model.md) for the shared symbols and Model contract. :::info Formula source Reviewed against [awesome-fsrs Wiki — FSRS-3](https://github.com/open-spaced-repetition/awesome-fsrs/wiki/The-Algorithm#fsrs-v3). ::: ## Forgetting curve and interval ```math R(t,S)=0.9^{t/S} ``` ```math I(r,S)=S\frac{\ln r}{\ln 0.9} ``` ## Initial memory ```math S_0(G)=\operatorname{clamp}\left(w_0+(G-1)w_1,S_{min},S_{max}\right) ``` ```math D_0(G)=\operatorname{clamp}\left(w_2+(G-3)w_3,D_{min},D_{max}\right) ``` ## Difficulty ```math D^*=D+w_4(G-3) ``` ```math D'=\operatorname{clamp}\left(w_5w_2+(1-w_5)D^*,D_{min},D_{max}\right) ``` ## Stability after review ```math S_f=w_9(D')^{w_{10}}S^{w_{11}}e^{w_{12}(1-R)} ``` ```math S_r=S\left[1+e^{w_6}(11-D')S^{w_7}\left(e^{w_8(1-R)}-1\right)\right] ``` ```math S'_{raw}(G)= \begin{cases} S_f,&G=1\\ S_r,&G\in\{2,3,4\} \end{cases} ``` The model then clamps `math:S'_{\mathrm{raw}}` to the range from `math:S_{\min}` to `math:S_{\max}`. --- url: https://open-spaced-repetition.github.io/ts-fsrs/guide/chrono/index.md --- > For AI agents: the complete documentation index is available at https://open-spaced-repetition.github.io/ts-fsrs/llms.txt, the full documentation bundle is available at https://open-spaced-repetition.github.io/ts-fsrs/llms-full.txt. # Chrono overview Chrono defines how a scheduler represents time, measures elapsed days, and calculates the next due time. The model works with intervals in days; Chrono connects those intervals to your application's time values. ## Choose a preset Import `dateChrono`, `numericChrono`, `temporalInstantChrono`, and `defineChrono` from `ts-fsrs`. | Preset | Time values | Day arithmetic | | ----------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `dateChrono` | JavaScript `Date` | `difference` counts UTC date boundaries by default; with `fractionalDays: true`, it measures elapsed time in fixed 24-hour days. `add` always adds fixed 24-hour days, including fractions. | | `numericChrono` | Numbers in days | `difference(from, to)` is `to - from`; `add(from, days)` is `from + days`. | | `temporalInstantChrono` | `Temporal.Instant` | Supports a configured timezone and optional fractional day differences. | See the preset guides for details: [Date](https://open-spaced-repetition.github.io/ts-fsrs/guide/chrono/date.md), [Numeric](https://open-spaced-repetition.github.io/ts-fsrs/guide/chrono/numeric.md), and [Temporal Instant](https://open-spaced-repetition.github.io/ts-fsrs/guide/chrono/temporal-instant.md). ## Definition and runtime operations A `Chrono` definition contains `schema`, `normalize`, `defaultValue`, and `create`. Pass the definition to `defineScheduler({ model, chrono })`; call its `create({ config })` when you need the time operations directly. The scheduler also exposes its configured runtime as `scheduler.chrono`. For Date and Temporal, pass the complete configuration to `create({ config })`; only presets without configuration, such as Numeric, support `create()` without arguments. Scheduler creation validates configuration and supplies schema defaults. | Operation | Meaning | | ---------------------- | ------------------------------------------------------------------------------------------- | | `now()` | Supplies a time value when the caller omits `now`. | | `difference(from, to)` | Returns elapsed days for the model. | | `add(from, days)` | Converts the final interval in days into a due time. | | `compare(left, right)` | Optional exact ordering: negative, zero, or positive. Date and Temporal presets provide it. | ## What normalize does `normalize` is a `ChronoTimeNormalizer` Standard Schema whose output is `ChronoNormalizedTime