Diagnosing a mid-interaction freeze in a SCORM package

Stack: SCORM 1.2 RTE, Storyline 360, Chrome DevTools, Moodle

The symptom

K-12 modules in production would freeze mid-interaction:

Refreshing reset the slide. Sometimes worked second time, sometimes not. The bug had been escalating as individual cases for weeks. The standard read was "the LMS is being weird."

What the team had tried

Re-publishing from Storyline (didn't help). Asking schools to clear cache (helped briefly, then recurred). Blaming the LMS (LMS team blamed Storyline). Replacing the affected interactions one at a time (didn't reproduce in QA).

This is the class of bug that gets called "the LMS is being weird" until someone has time to instrument it.

The diagnosis

I patched the SCORM API wrapper to log every LMSSetValue and LMSGetValue to the console, then had two learners reproduce the freeze while I watched:

const realSet = window.API.LMSSetValue;
window.API.LMSSetValue = function (k, v) {
  console.log("SET", k, v);
  return realSet.call(window.API, k, v);
};

The log was clear: on the failing interactions, cmi.interactions.N.id was being set twice for the same N within a few milliseconds.

First call: correct id, returns "true". Second call: same id, returns "false" — the LMS, conforming to the SCORM 1.2 RTE spec, rejects a second write to a locked interaction. Storyline's player saw the false return, interpreted it as "interaction not recorded," and refused to enable the Next button.

The duplicate write came from a slide that combined a Storyline interactive element with a custom narration hook we'd added. Both layers were calling the interaction accessor on the same item. Whichever fired second got the false and locked the Next button.

The fix

Two-line guard in the custom layer:

if (alreadyReportedThisInteraction(slideId, itemId)) return;
markInteractionReported(slideId, itemId);

Plus a backstop in the SCORM API wrapper itself: if LMSSetValue returns false on an interactions.N.id we've already written, treat it as a no-op rather than letting the caller interpret it as failure.

What I learned

A spec-compliant LMS, a spec-compliant authoring tool, and custom glue code can each be doing the right thing in isolation and still produce a bug. The freeze was opaque for weeks because nobody had thought to log the SCORM API calls. Once instrumented, it was a 20-minute diagnosis.

Build-time auditing for duplicate interaction IDs is now part of our pipeline — it catches the same class of bug before it ships. That auditor became the basis for the open-source scorm-lint.

The LMSSetValue failure return is one of the more useful and more ignored signals in the SCORM 1.2 RTE spec. Most authoring tools and most LMS integrations treat it as decorative. It isn't.

Why this is in the portfolio

It's a mundane class of bug, but reading a cmi.interactions.N.* trace and explaining what's happening is the kind of thing that distinguishes someone who has worked at the SCORM API level from someone who has only worked at the "publish-and-pray" level. Three months on, the auditor has prevented recurrence.