Skip to content

In-session module-plugin upgrade orphans the old instance — stale audio graph keeps playing (stems) #1015

Description

@topkoa

Summary

The plugin loader supports upgrading a plugin within a running session — but for
scriptType: "module" plugins that's unsound: an ES module can't be un-executed. The old
module instance survives the "upgrade" with all its live state. For a stateful audio plugin
(stems), that means an orphaned audio graph that keeps playing alongside the new
instance's graph.

Mechanism

static/js/plugin-loader.js (~645–682):

  • loadedScripts.get(plugin.id) !== wantedVersion → on a version change it calls
    _removePluginScriptTags(plugin.id) and injects screen.js?v=<newVersion> (cache-bust is
    deliberate: "so a plugin upgrade within the same browser session fetches the new screen.js").
  • Removing a <script type="module"> tag does not tear down the already-evaluated module.
    The old instance's module state, event listeners, capability registrations, and Web Audio
    nodes all stay live.

For the stems plugin specifically (plugins/stems, scriptType: "module"):

  • Both instances observe the playback lifecycle → both build audio graphs on the next
    song (double audio, initially sample-aligned so it's easy to miss).
  • Only the newest instance installs the #audio shims, so transport commands (seek/pause/
    rate) reach only it. After a seek, the orphan keeps playing from the pre-seek position —
    audibly desynced vocals/instruments while the on-screen clock, notes, and lyrics follow the
    live instance.

Any stateful module plugin is affected (audio graphs, MutationObservers, WS handlers,
window.playSong wraps); audio is just the loudest failure mode.

When it triggers

Any in-session version change picked up by _refreshPluginsSoon / loadPlugins(): Plugin
Manager updates, and in dev, a git pull in a plugin dir while the app is running (this repo
bumped stems to 0.9.0 today — a running session would have orphaned 0.8.2).

Possibly related field report (unconfirmed link)

Persistent post-fast-forward lyric/vocal desync observed on one song ("Marks Of The Evil
One", zip pack). Pack data was verified internally consistent (lyrics ↔ vocals stem ↔ full
mix cross-correlated within ~9 ms across the whole song), and the lyric renderer is
time-faithful — the symptom matches the orphan mechanism (audio from the wrong position
after seek while the clock is right). However the report claims it survived a full app
restart, which an orphan should not — so this issue is filed for the loader hole on its own
merits; the field bug may have an additional cause. Repro attempts ongoing.

Suggested directions

  • Minimum: make module-plugin upgrade not silently unsound — e.g. require a
    window.__<plugin>Instance.destroy()-style handshake (stem_mixer already implements this
    pattern) before injecting the new version, and skip the upgrade (with a "reload required"
    notice) when the plugin doesn't expose one.
  • Alternatively: on any module-plugin version change, prompt/force a renderer reload instead
    of hot-swapping.
  • The plugin-runtime-idempotent.v1 standard could grow an explicit teardown contract for
    module plugins (the module-cache idempotency it relies on only covers same-version
    re-injection, not upgrades).

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions