How can we make FSRS actually schedule the cards?

How can we make FSRS actually schedule the cards when they drop below the desired retention. At the moment, I have around 10% of my deck that its’ target R% is under 90% (I am using the beta that was released most recently, Version ⁨25.06 (c1fc4592)), yet it doesn’t show it to me on that day, instead the interval takes over.

For example, I have some cards what with a 1d interval they’d drop to 70-80%, my desired R is 90%. Obviously, anki should schedule it in the same day possible. Maybe this is too advanced, but my other issue is that

these same cards with the 1d interval, if they survive and get a good grade, they become 2d intervals immediately. Yet, their target R is 70%, and I don’t get them 1d after, when it would be. at 80-90%, instead I get them at 2days at 70-80%.

I was told, it was because of this. Personally, good should not be an automatic +1 than hard button.


Is there a way for me to disable this +1 thing, or an addon that can overwrite this? Or is it not possible to write an addon.


06-06 + 2 days → 06-08,


Yet, the 90% r drops on the 7th and I don’t get to see it until it is

at 80%.

Is correcting for the error by setting a higher DR an option?

It could be, but that changes all the cards.

I only have this issue with short term cards

we might get the per-deck DR feature in next version (if luc completes it) so that would help.

this didn’t caught my attention before, have you tried custom scheduling (not sure if this rule is hard coded though)? a nice post detailing some basics: Custom Scheduling Help - #3 by jhhr

Yeah, this can be done with custom scheduling. Try the code below and test if it works as you expect. I copied parts of my own current custom scheduling code and edited to fit this use case. This was a fun exercise and I learned that things are changing in FSRS-6 with the decay parameter becoming something that can be optimized. I may use this code myself - I already updated my own factor and decay calculations from doing this.

It’s not possible to set review intervals (the schedulesDays value) to anything but whole numbers. Only learning/relearning steps can be <1 day. It’s possible to insert custom relearning steps in the custom scheduling as well and the code below sets intervals <1 by doing that.

Necessary steps to enable the code

  • First, copy the code into the custom scheduling section in the deck settings. Note that custom scheduling code applies to all decks. For that reason the code includes deck settings (based on the old FSRS helper custom scheduling by LMSherlock)
  • Add <div id="deck" deck_name="{{Deck}}"></div> to the Front template in each card type you want to use the custom scheduling code. This is needed for the deck settings. The deck settings are there so that you turn off the functionality for some decks and set the desired retention correctly for each of your decks.
  • If you use 0.9 DR in all decks and want to use this functionality everywhere you only need to set desiredRetention: 0.9 in the global deck config in custom scheduling code
  • If you set DR in global config but don’t want use the functionality in specific decks add those deck names to skipDecks
  • If you need to set different DR levels for specific decks, input those decks into the decks settings and add the desiredRetention value
The custom scheduling code
const LOG = false; // Set to true to enable logging for debugging

// Don't adjust intervals for new or new cards still in learning steps
if (
  states.current.normal?.new ||
  states.current.normal?.learning ||
  states.current.filtered?.rescheduling?.originalState?.new ||
  states.current.filtered?.rescheduling?.originalState?.learning
) {
  return;
}


const deckParams = [
  // Default parameters used in all decks that aren't skipped
  // You don't need to define all parameters for each deck, only the ones you want to override
  {
    deckName: "global config for Custom Scheduler",
    // desired retention will need to be copied from each deck's settings because the custom scheduler
    // doesn't have access to the deck's settings
    // whitelist mode: leave this undefined --> only decks in the deckParams list will be adjusted
    // blacklist mode: set this to a value > 0.1 and < 0.99 --> all decks will be adjusted except
    // those that you add to the skipDecks list below
    desiredRetention: undefined,
    // Decay parameter for the retention formula
    // In FSRS-6 (Anki 25.05+) this will be the 21st (last) param in FSRS parameters in the deck 
    // settings. After that update, you'd need to copy that value for each deck to get the correct
    // retention calculation.
    // Leave undefined if on Anki <25.05 to use FSRS-4.5 formula
    decayParam: undefined,
  },
  {
    deckName: "Example Deck",
    desiredRetention: 0.9,
  },
];

// To not use custom scheduling in specific decks, fill them into the skip_decks list below.
// Please don't remove it even if you don't need it.
const skipDecks = [];


function getDeckname() {
  return (
    ctx?.deckName || document.getElementById("deck")?.getAttribute("deckName")
  );
}

const globalDeckParams = deckParams.find(
  (deck) => deck.deckName === "global config for Custom Scheduler"
);
if (!globalDeckParams) {
  if (displaySchedulerState) {
    schedulerStatus.innerHTML +=
      '<br><span style="color:red;">ERROR: Global config not found</span>';
  }
}
let currentDeckParams = globalDeckParams;

let deckName = getDeckname();
if (!deckName) {
} else {
  if (skipDecks.some((skipDeck) => deckName.startsWith(skipDeck))) {
    return;
  }
  // Arrange the deckParams of parent decks in front of their sub decks.
  // This is so that we can define parameters for a parent deck and have them apply to all
  // sub-decks while still being able to override them for specific sub-decks without
  // having to define the same parameters for each sub-deck.
  deckParams.sort(function (a, b) {
    return a.deckName.localeCompare(b.deckName);
  });
  for (let i = 0; i < deckParams.length; i++) {
    if (deckName.startsWith(deckParams[i]["deckName"])) {
      foundParams = true;
      currentDeckParams = {
        ...currentDeckParams,
        ...deckParams[i]
      };
      // continue looping and overwriting the parameters with the next matching sub-deck's
      // parameters, if there are any
    }
  }
}

// Get params for current deck
const { decayParam, desiredRetention } = currentDeckParams;

if (!Number.isFinite(desiredRetention) || desiredRetention < 0.1 || desiredRetention > 0.99) {
  // If desired retention is not set or invalid, do not adjust intervals
  if (LOG) console.log("Invalid desired retention, skipping interval adjustment");
  return;
}
if (!Number.isFinite(decayParam) || decayParam <= 0) {
  // If decay parameter is not set or invalid, do not adjust intervals
  if (LOG) console.log("Invalid decay parameter, skipping interval adjustment");
  return;
}

const againRevObj =
  states.again.normal?.review ||
  states.again.normal?.relearning?.review ||
  states.again.filtered?.rescheduling?.originalState?.review ||
  states.again.filtered?.rescheduling?.originalState?.relearning?.review;
const baseAgainObj =
  states.again.normal || states.again.filtered?.rescheduling?.originalState;
const againRelearnObj =
  states.again.normal?.relearning?.learning ||
  states.again.filtered?.rescheduling?.originalState?.relearning?.learning;
const hardRevObj =
  states.hard.normal?.review ||
  states.hard.filtered?.rescheduling?.originalState?.review ||
  states.hard.normal?.relearning?.review ||
  states.hard.filtered?.rescheduling?.originalState?.relearning?.review;
const baseHardObj =
  states.hard.normal || states.hard.filtered?.rescheduling?.originalState;
const hardRelearnObj =
  states.hard.normal?.relearning?.learning ||
  states.hard.filtered?.rescheduling?.originalState?.relearning?.learning;
const goodRevObj =
  states.good.normal?.review ||
  states.good.filtered?.rescheduling?.originalState?.review ||
  states.good.normal?.relearning?.review ||
  states.good.filtered?.rescheduling?.originalState?.relearning?.review;
const goodRelearnObj =
  states.good.normal?.relearning?.learning ||
  states.good.filtered?.rescheduling?.originalState?.relearning?.learning;
const baseGoodObj =
  states.good.normal || states.good.filtered?.rescheduling?.originalState;
const easyRevObj =
  states.easy.normal?.review ||
  states.easy.filtered?.rescheduling?.originalState?.review;
const baseEasyObj =
  states.easy.normal || states.easy.filtered?.rescheduling?.originalState;


// If relearning steps are defined in the deck settings, we will use them, otherwise the interval
// modifications below would overwrite those

// When there are more than 1 relearning steps and you answer again, the next review will have
// the good answer be a relearning step
const previousReviewWasDeckDefinedRelearn = !!goodRelearnObj;
// If we are in a review, then deck defined relearning steps will be present in the again answer
const isReview = !goodRelearnObj && !!goodRevObj;
const isReviewButHasDeckDefinedRelearn = isReview && !!againRelearnObj;
if (previousReviewWasDeckDefinedRelearn || isReviewButHasDeckDefinedRelearn) {
  if (LOG) console.log("relearning steps defined for deck, skipping interval modifications");
  return;
}

// FSRS-4.5:
let FACTOR = 19 / 81;
let DECAY = -0.5;
if (Number.isFinite(decayParam)) {
  // FSRS-5/6
  FACTOR = 0.9 ** (-1 / decayParam);
  DECAY = -decayParam;
}
/**
 * Calculate the interval for a given stability and desired retention.
 * 
 * @param {number} stability - Whole number, days
 * @param {number} retention - Float, between 0 and 1 
 * @returns {number|undefined} - The calculated interval in days, potentially <1,
 *   or undefined if inputs are invalid.
 */
function calcIvlForRetention(stability, retention) {
  if (!Number.isFinite(stability) || !Number.isFinite(retention) || retention <= 0.1) {
    return undefined;
  }
  if (LOG) console.log("calcIvlForRetention", stability, retention);

  // Return interval without rounding to an integer
  return (stability / FACTOR) * (retention ** (1 / DECAY) - 1);
}

/**
 * Add a relearning step to the base object if it doesn't already have one.
 * 
 * @param {object} baseObj - The current answer object
 * @param {object} revObj  - The answer button object
 * @param {number} remainingSteps - The number of remaining steps to set
 * @param {number} ivlInDays - The interval in days (non-whole number) for the relearning step
 * @param {object} [curRelearnObj] - The current relearning object, if any
 */
function addRelearnStep(baseObj, revObj, ivlInDays, remainingSteps, curRelearnObj) {
  if (LOG) console.log("addRelearnStep, ivlInDays", ivlInDays);
  const scheduledSecs = Math.ceil(ivlInDays * 24 * 60 * 60);
  // Make relearning object to add to the base object
  const relearnObj = {
    scheduledSecs,
    elapsedSecs: 0,
    remainingSteps,
    memoryState: curRelearnObj?.memoryState
      ? curRelearnObj.memoryState
      : revObj.memoryState
  };

  // add .relearning to the baseObj and move revObj to it
  baseObj.relearning = {
    review: revObj,
    learning: relearnObj
  };
  // Remove the review from the baseObj, making this answer a relearning step
  delete baseObj.review;
}

/**
 * Set the interval for review based on the stability and desired retention.
 * 
 * @param {object} baseObj - The current answer object
 * @param {object} revObj  - The answer button object
 * @param {object} relearnObj - The relearning object, if any
 * @param {number} remainingSteps - The number of remaining steps to set
 * @param {number} [minIvlInDays=1] - Integer, the minimum interval in days for the review.
 *   Only applied when the calculated interval is >= 1
 **/
function setIntervalForReview(baseObj, revObj, relearnObj, remainingSteps, minIvlInDays = 1) {
  // Sanity check, do nothing if args are not valid
  if (typeof baseObj !== "object"
    || !baseObj
    || typeof revObj !== "object"
    || !revObj
    || !Number.isFinite(remainingSteps)
    || remainingSteps < 0
    || (relearnObj && typeof relearnObj !== "object")
    || !Number.isFinite(minIvlInDays)
    || minIvlInDays < 1) {
    return;
  }
  // Check revObj has a memoryState object; FSRS should be enabled
  if (!revObj.memoryState || !Number.isFinite(revObj.memoryState.stability)) {
    if (LOG) console.log("addRelearnStep: revObj does not have a memoryState object, skipping");
    return;
  }

  // Calculate the interval in days based on the stability and desired retention
  const ivlInDays = calcIvlForRetention(revObj.memoryState.stability, desiredRetention);
  if (!Number.isFinite(ivlInDays) || ivlInDays < 0) {
    if (LOG) console.log("addRelearnStep: invalid ivlInDays, skipping");
    return;
  }
  if (LOG) console.log("setIntervalForReview: ivlInDays", ivlInDays);
  // If the interval is less than 1 day, we need to add a relearning step
  if (ivlInDays < 1) {
    addRelearnStep(baseObj, revObj, ivlInDays, remainingSteps, relearnObj);
    // Anki's default behavior is to to always round up making Retrievability always < DR when
    // a card is due for review

    // We'll instead set the scheduledDays to the rounded interval
    // By rounding, we set it closer to the expected retention but this will cause the
    // Retrivievabilty to be a bit higher than the target retention when we round down, the
    // difference being largest when intervals are small
    revObj.scheduledDays = Math.max(Math.round(ivlInDays), minIvlInDays);
    if (LOG) console.log("setIntervalForReview: scheduledDays", revObj.scheduledDays);
  }
}

// If hard interval is 1 day, by default good interval will be at least 2 days
// In such cases the good interval may occur at a time when expected retention is much lower
// than the target retention
// To fix this we will calculate the good & easy intervals based on the desired retention
// only if the hard interval is 1 day (indicating we are at low stability)
if (LOG) console.log("setting interval for again, before edit", JSON.stringify(baseAgainObj, null, 2));
setIntervalForReview(baseAgainObj, againRevObj, againRelearnObj, 1);
if (LOG) console.log("again, after edit", JSON.stringify(baseAgainObj, null, 2));
if (LOG) console.log("setting interval for hard, before edit", JSON.stringify(baseHardObj, null, 2));
setIntervalForReview(baseHardObj, hardRevObj, hardRelearnObj, 1);
if (LOG) console.log("hard, after edit", JSON.stringify(baseHardObj, null, 2));
if (LOG) console.log("setting interval for good, before edit", JSON.stringify(baseGoodObj, null, 2));
setIntervalForReview(baseGoodObj, goodRevObj, null, 0);
if (LOG) console.log("good, after edit", JSON.stringify(baseGoodObj, null, 2));
// Set easy interval to good + 1, as it doesn't make sense to have both good and easy be the same
// This is only done when the modified good interval was recalculated as at least 1 day.
// If the good interval is less than 1 day, the easy interval will either be less than 1 day
// too but still greater than the good interval, or it will be >= 1 day
const minEasyIvl = Number.isFinite(baseGoodObj.review?.scheduledDays)
  ? baseGoodObj.review.scheduledDays + 1
  : undefined;
if (LOG) console.log("setting interval for easy", JSON.stringify(baseEasyObj, null, 2));
setIntervalForReview(baseEasyObj, easyRevObj, null, 0, minEasyIvl);
if (LOG) console.log("easy, after edit", JSON.stringify(baseEasyObj, null, 2));

Edit: Fixed FACTOR using FSRS-6 formula pre-emptively, should still use FACTOR = 19 / 81 while on Anki 25.02.x

2 Likes

tagged the right person🧎‍♂️

the code uses load balancer and fuzz though?

Oh yeah, fuzz & load balancing. If I understand correctly load balancing is calculated by Anki into the initial intervals that the custom scheduling code then receives. So modifying those will lose load balancing.

Fuzz can be added.

Here’s a version that’s adds ±5% fuzz for raw intervals > 2.5. Also based on Jarret’s old FSRS helper code. With a deck setting useFuzz to turn fuzz on or off. I also added a new deck param adjustIvlMax for limiting how large intervals should be modified. I guess that should be set to something like 2-5 for the use case in this thread; depending on what intervals @dyzur thinks Good should not be Hard + 1.

Custom scheduling with (optional) fuzz & option to limit ivl adjustment
const LOG = false; // Set to true to enable logging for debugging

// Don't adjust intervals for new or new cards still in learning steps
if (
  states.current.normal?.new ||
  states.current.normal?.learning ||
  states.current.filtered?.rescheduling?.originalState?.new ||
  states.current.filtered?.rescheduling?.originalState?.learning
) {
  return;
}


const deckParams = [
  // Default parameters used in all decks that aren't skipped
  // You don't need to define all parameters for each deck, only the ones you want to override
  {
    deckName: "global config for Custom Scheduler",
    // desired retention will need to be copied from each deck's settings because the custom scheduler
    // doesn't have access to the deck's settings
    // whitelist mode: leave this undefined --> only decks in the deckParams list will be adjusted
    // blacklist mode: set this to a value > 0.1 and < 0.99 --> all decks will be adjusted except
    // those that you add to the skipDecks list below
    desiredRetention: undefined,
    // Decay parameter for the retention formula
    // In FSRS-6 (Anki 25.05+) this will be the 21st (last) param in FSRS parameters in the deck 
    // settings. After that update, you'd need to copy that value for each deck to get the correct
    // retention calculation.
    // Leave undefined if on Anki <25.05 to use FSRS-4.5 formula
    decayParam: undefined,
    // Set this to a value > 1 to only adjust intervals less than that. This can be used to
    // maintain load balancing that is applied by Anki into the default intervals.
    // If left undefined, all intervals will be adjusted
    adjustIvlMax: undefined,
    // Set to false to disable fuzzing of intervals
    useFuzz: true,
  },
  {
    deckName: "Example Deck",
    desiredRetention: 0.9,
    adjustIvlMax: 2, // Only adjust intervals less than 2 days
  },
];

// To not use custom scheduling in specific decks, fill them into the skip_decks list below.
// Please don't remove it even if you don't need it.
const skipDecks = [];


function getDeckname() {
  return (
    ctx?.deckName || document.getElementById("deck")?.getAttribute("deckName")
  );
}

const globalDeckParams = deckParams.find(
  (deck) => deck.deckName === "global config for Custom Scheduler"
);
if (!globalDeckParams) {
  if (displaySchedulerState) {
    schedulerStatus.innerHTML +=
      '<br><span style="color:red;">ERROR: Global config not found</span>';
  }
}
let currentDeckParams = globalDeckParams;

let deckName = getDeckname();
if (!deckName) {
} else {
  if (skipDecks.some((skipDeck) => deckName.startsWith(skipDeck))) {
    return;
  }
  // Arrange the deckParams of parent decks in front of their sub decks.
  // This is so that we can define parameters for a parent deck and have them apply to all
  // sub-decks while still being able to override them for specific sub-decks without
  // having to define the same parameters for each sub-deck.
  deckParams.sort(function (a, b) {
    return a.deckName.localeCompare(b.deckName);
  });
  for (let i = 0; i < deckParams.length; i++) {
    if (deckName.startsWith(deckParams[i]["deckName"])) {
      foundParams = true;
      currentDeckParams = {
        ...currentDeckParams,
        ...deckParams[i]
      };
      // continue looping and overwriting the parameters with the next matching sub-deck's
      // parameters, if there are any
    }
  }
}

// Get params for current deck
const {
  decayParam: DECK_DECAY,
  desiredRetention: DECK_DR,
  adjustIvlMax: DECK_MAX_ADJUST_IVL,
  useFuzz: DECK_USE_FUZZ,
} = currentDeckParams;

if (!Number.isFinite(DECK_DR) || DECK_DR < 0.1 || DECK_DR > 0.99) {
  // If desired retention is not set or invalid, do not adjust intervals
  if (LOG) console.log("Invalid desired retention, skipping interval adjustment");
  return;
}
if (!Number.isFinite(DECK_DECAY) || DECK_DECAY <= 0) {
  // If decay parameter is not set or invalid, do not adjust intervals
  if (LOG) console.log("Invalid decay parameter, skipping interval adjustment");
  return;
}

const againRevObj =
  states.again.normal?.review ||
  states.again.normal?.relearning?.review ||
  states.again.filtered?.rescheduling?.originalState?.review ||
  states.again.filtered?.rescheduling?.originalState?.relearning?.review;
const baseAgainObj =
  states.again.normal || states.again.filtered?.rescheduling?.originalState;
const againRelearnObj =
  states.again.normal?.relearning?.learning ||
  states.again.filtered?.rescheduling?.originalState?.relearning?.learning;
const hardRevObj =
  states.hard.normal?.review ||
  states.hard.filtered?.rescheduling?.originalState?.review ||
  states.hard.normal?.relearning?.review ||
  states.hard.filtered?.rescheduling?.originalState?.relearning?.review;
const baseHardObj =
  states.hard.normal || states.hard.filtered?.rescheduling?.originalState;
const hardRelearnObj =
  states.hard.normal?.relearning?.learning ||
  states.hard.filtered?.rescheduling?.originalState?.relearning?.learning;
const goodRevObj =
  states.good.normal?.review ||
  states.good.filtered?.rescheduling?.originalState?.review ||
  states.good.normal?.relearning?.review ||
  states.good.filtered?.rescheduling?.originalState?.relearning?.review;
const goodRelearnObj =
  states.good.normal?.relearning?.learning ||
  states.good.filtered?.rescheduling?.originalState?.relearning?.learning;
const baseGoodObj =
  states.good.normal || states.good.filtered?.rescheduling?.originalState;
const easyRevObj =
  states.easy.normal?.review ||
  states.easy.filtered?.rescheduling?.originalState?.review;
const baseEasyObj =
  states.easy.normal || states.easy.filtered?.rescheduling?.originalState;


// If relearning steps are defined in the deck settings, we will use them, otherwise the interval
// modifications below would overwrite those

// When there are more than 1 relearning steps and you answer again, the next review will have
// the good answer be a relearning step
const previousReviewWasDeckDefinedRelearn = !!goodRelearnObj;
// If we are in a review, then deck defined relearning steps will be present in the again answer
const isReview = !goodRelearnObj && !!goodRevObj;
const isReviewButHasDeckDefinedRelearn = isReview && !!againRelearnObj;
if (previousReviewWasDeckDefinedRelearn || isReviewButHasDeckDefinedRelearn) {
  if (LOG) console.log("relearning steps defined for deck, skipping interval modifications");
  return;
}
// FSRS-4.5:
let FACTOR = 19 / 81;
let DECAY = -0.5;
if (Number.isFinite(decayParam)) {
  // FSRS-5/6
  FACTOR = 0.9 ** (-1 / decayParam);
  DECAY = -decayParam;
}
/**
 * Calculate the interval for a given stability and desired retention.
 * 
 * @param {number} stability - Whole number, days
 * @param {number} retention - Float, between 0 and 1 
 * @returns {number|undefined} - The calculated interval in days, potentially <1,
 *   or undefined if inputs are invalid.
 */
function calcIvlForRetention(stability, retention) {
  if (!Number.isFinite(stability) || !Number.isFinite(retention) || retention <= 0.1) {
    return undefined;
  }
  if (LOG) console.log("calcIvlForRetention", stability, retention);

  // Return interval without rounding to an integer
  return (stability / FACTOR) * (retention ** (1 / DECAY) - 1);
}

/**
 * Add a relearning step to the base object if it doesn't already have one.
 * 
 * @param {object} baseObj - The current answer object
 * @param {object} revObj  - The answer button object
 * @param {number} remainingSteps - The number of remaining steps to set
 * @param {number} ivlInDays - The interval in days (non-whole number) for the relearning step
 * @param {object} [curRelearnObj] - The current relearning object, if any
 */
function addRelearnStep(baseObj, revObj, ivlInDays, remainingSteps, curRelearnObj) {
  if (LOG) console.log("addRelearnStep, ivlInDays", ivlInDays);
  const scheduledSecs = Math.ceil(ivlInDays * 24 * 60 * 60);
  // Make relearning object to add to the base object
  const relearnObj = {
    scheduledSecs,
    elapsedSecs: 0,
    remainingSteps,
    memoryState: curRelearnObj?.memoryState
      ? curRelearnObj.memoryState
      : revObj.memoryState
  };

  // add .relearning to the baseObj and move revObj to it
  baseObj.relearning = {
    review: revObj,
    learning: relearnObj
  };
  // Remove the review from the baseObj, making this answer a relearning step
  delete baseObj.review;
}


// Global fuzz factor for all ratings.
const fuzzFactor = setFuzzFactor();
if (LOG) console.log("fuzzFactor", fuzzFactor);
function applyFuzz(ivl) {
  if (!DECK_USE_FUZZ) return Math.round(ivl);
  if (ivl < 2.5) return ivl;
  ivl = Math.round(ivl);
  let min_ivl = Math.max(2, Math.round(ivl * 0.95 - 1));
  let max_ivl = Math.round(ivl * 1.05 + 1);
  return Math.floor(fuzzFactor * (max_ivl - min_ivl + 1) + min_ivl);
}
function get_seed() {
  if (!customData.again.s | !customData.hard.s | !customData.good.s | !customData.easy.s) {
    if (typeof ctx !== 'undefined' && ctx.seed) {
      return ctx.seed;
    } else {
      return document.getElementById("qa").innerText;
    }
  } else {
    return customData.good.s;
  }
}
function setFuzzFactor() {
  // Note: Originally copied from seedrandom.js package (https://github.com/davidbau/seedrandom)
  !function (f, a, c) { var s, l = 256, p = "random", d = c.pow(l, 6), g = c.pow(2, 52), y = 2 * g, h = l - 1; function n(n, t, r) { function e() { for (var n = u.g(6), t = d, r = 0; n < g;)n = (n + r) * l, t *= l, r = u.g(1); for (; y <= n;)n /= 2, t /= 2, r >>>= 1; return (n + r) / t } var o = [], i = j(function n(t, r) { var e, o = [], i = typeof t; if (r && "object" == i) for (e in t) try { o.push(n(t[e], r - 1)) } catch (n) { } return o.length ? o : "string" == i ? t : t + "\0" }((t = 1 == t ? { entropy: !0 } : t || {}).entropy ? [n, S(a)] : null == n ? function () { try { var n; return s && (n = s.randomBytes) ? n = n(l) : (n = new Uint8Array(l), (f.crypto || f.msCrypto).getRandomValues(n)), S(n) } catch (n) { var t = f.navigator, r = t && t.plugins; return [+new Date, f, r, f.screen, S(a)] } }() : n, 3), o), u = new m(o); return e.int32 = function () { return 0 | u.g(4) }, e.quick = function () { return u.g(4) / 4294967296 }, e.double = e, j(S(u.S), a), (t.pass || r || function (n, t, r, e) { return e && (e.S && v(e, u), n.state = function () { return v(u, {}) }), r ? (c[p] = n, t) : n })(e, i, "global" in t ? t.global : this == c, t.state) } function m(n) { var t, r = n.length, u = this, e = 0, o = u.i = u.j = 0, i = u.S = []; for (r || (n = [r++]); e < l;)i[e] = e++; for (e = 0; e < l; e++)i[e] = i[o = h & o + n[e % r] + (t = i[e])], i[o] = t; (u.g = function (n) { for (var t, r = 0, e = u.i, o = u.j, i = u.S; n--;)t = i[e = h & e + 1], r = r * l + i[h & (i[e] = i[o = h & o + t]) + (i[o] = t)]; return u.i = e, u.j = o, r })(l) } function v(n, t) { return t.i = n.i, t.j = n.j, t.S = n.S.slice(), t } function j(n, t) { for (var r, e = n + "", o = 0; o < e.length;)t[h & o] = h & (r ^= 19 * t[h & o]) + e.charCodeAt(o++); return S(t) } function S(n) { return String.fromCharCode.apply(0, n) } if (j(c.random(), a), "object" == typeof module && module.exports) { module.exports = n; try { s = require("crypto") } catch (n) { } } else "function" == typeof define && define.amd ? define(function () { return n }) : c["seed" + p] = n }("undefined" != typeof self ? self : this, [], Math);
  // MIT License
  // Copyright 2019 David Bau.
  // Permission is hereby granted, free of charge, to any person obtaining a copy
  // of this software and associated documentation files (the "Software"), to deal
  // in the Software without restriction, including without limitation the rights
  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  // copies of the Software, and to permit persons to whom the Software is
  // furnished to do so, subject to the following conditions:
  // The above copyright notice and this permission notice shall be included in all
  // copies or substantial portions of the Software.
  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  // SOFTWARE.
  let seed = get_seed();
  const generator = new Math.seedrandom(seed);
  const fuzz_factor = generator();
  seed = Math.round(fuzz_factor * 10000);
  customData.again.s = (seed + 1) % 10000;
  customData.hard.s = (seed + 2) % 10000;
  customData.good.s = (seed + 3) % 10000;
  customData.easy.s = (seed + 4) % 10000;
  return fuzz_factor;
}

/**
 * Set the interval for review based on the stability and desired retention.
 * 
 * @param {object} baseObj - The current answer object
 * @param {object} revObj  - The answer button object
 * @param {object} relearnObj - The relearning object, if any
 * @param {number} remainingSteps - The number of remaining steps to set
 * @param {number} [minIvlInDays=1] - Integer, the minimum interval in days for the review.
 *   Only applied when the calculated interval is >= 1
 **/
function setIntervalForReview(baseObj, revObj, relearnObj, remainingSteps, minIvlInDays = 1) {
  // Sanity check, do nothing if args are not valid
  if (typeof baseObj !== "object"
    || !baseObj
    || typeof revObj !== "object"
    || !revObj
    || !Number.isFinite(remainingSteps)
    || remainingSteps < 0
    || (relearnObj && typeof relearnObj !== "object")
    || !Number.isFinite(minIvlInDays)
    || minIvlInDays < 1) {
    return;
  }
  // Check revObj has a memoryState object; FSRS should be enabled
  if (!revObj.memoryState || !Number.isFinite(revObj.memoryState.stability)) {
    if (LOG) console.log("setIntervalForReview: revObj does not have a memoryState object, skipping");
    return;
  }

  const curIvl = revObj.scheduledDays;
  if (Number.isFinite(DECK_MAX_ADJUST_IVL) && curIvl >= DECK_MAX_ADJUST_IVL) {
    if (LOG) console.log("setIntervalForReview: current interval is greater than or equal to adjustIvlMax, skipping");
    return;
  }

  // Calculate the interval in days based on the stability and desired retention
  const ivlInDays = calcIvlForRetention(revObj.memoryState.stability, DECK_DR);
  if (!Number.isFinite(ivlInDays) || ivlInDays < 0) {
    if (LOG) console.log("setIntervalForReview: invalid ivlInDays, skipping");
    return;
  }
  if (LOG) console.log("setIntervalForReview: ivlInDays", ivlInDays);
  // If the interval is less than 1 day, we need to add a relearning step
  if (ivlInDays < 1) {
    addRelearnStep(baseObj, revObj, ivlInDays, remainingSteps, relearnObj);
    // Anki's default behavior is to to always round up making Retrievability always < DR when
    // a card is due for review

    // We'll instead set the scheduledDays to the rounded interval
    // By rounding, we set it closer to the expected retention but this will cause the
    // Retrivievabilty to be a bit higher than the target retention when we round down, the
    // difference being largest when intervals are small
    revObj.scheduledDays = Math.max(applyFuzz(ivlInDays), minIvlInDays);
    if (LOG) console.log("setIntervalForReview: scheduledDays", revObj.scheduledDays);
  }
}

// If hard interval is 1 day, by default good interval will be at least 2 days
// In such cases the good interval may occur at a time when expected retention is much lower
// than the target retention
// To fix this we will calculate the good & easy intervals based on the desired retention
// only if the hard interval is 1 day (indicating we are at low stability)
if (LOG) console.log("setting interval for again, before edit", JSON.stringify(baseAgainObj, null, 2));
setIntervalForReview(baseAgainObj, againRevObj, againRelearnObj, 1);
if (LOG) console.log("again, after edit", JSON.stringify(baseAgainObj, null, 2));
if (LOG) console.log("setting interval for hard, before edit", JSON.stringify(baseHardObj, null, 2));
setIntervalForReview(baseHardObj, hardRevObj, hardRelearnObj, 1);
if (LOG) console.log("hard, after edit", JSON.stringify(baseHardObj, null, 2));
if (LOG) console.log("setting interval for good, before edit", JSON.stringify(baseGoodObj, null, 2));
setIntervalForReview(baseGoodObj, goodRevObj, null, 0);
if (LOG) console.log("good, after edit", JSON.stringify(baseGoodObj, null, 2));
// Set easy interval to good + 1, as it doesn't make sense to have both good and easy be the same
// This is only done when the modified good interval was recalculated as at least 1 day.
// If the good interval is less than 1 day, the easy interval will either be less than 1 day
// too but still greater than the good interval, or it will be >= 1 day
const minEasyIvl = Number.isFinite(baseGoodObj.review?.scheduledDays)
  ? baseGoodObj.review.scheduledDays + 1
  : undefined;
if (LOG) console.log("setting interval for easy", JSON.stringify(baseEasyObj, null, 2));
setIntervalForReview(baseEasyObj, easyRevObj, null, 0, minEasyIvl);
if (LOG) console.log("easy, after edit", JSON.stringify(baseEasyObj, null, 2));

Edit: Fixed FACTOR using FSRS-6 formula pre-emptively, should still use FACTOR = 19 / 81 while on Anki 25.02.x