Found by CodeRabbit during the R3a plugin-loader carve (#878). Pre-existing — it arrived with the R0 module rails (#812) and was moved verbatim, so it is not a regression from the carve.
The bug
static/js/plugin-loader.js builds the plugin script URL from the plugin's version:
script.src = `/api/plugins/${plugin.id}/screen.js${v ? `?v=${v}` : ''}`;
...
if (plugin.script_type === 'module') script.type = 'module';
ES modules are evaluated once per URL per document. Re-inserting a <script type="module"> whose src is already in the module map does not re-execute it.
So on a downgrade — rolling a plugin back to a version already evaluated this session — the URL is one the module map has seen, the script silently does not re-run, and loadedScripts records it as loaded anyway. The plugin screen stays stale.
Upgrades are fine (a new version yields a new URL). It is specifically the rollback / re-install-older path. Classic-script plugins are unaffected — only scriptType: "module" plugins.
Why it wasn't fixed in #878
That PR is a verbatim carve whose whole value is being provably behaviour-neutral. The fix is a real behaviour change to the plugin-loading path, and it is not a one-liner:
- a monotonic load token (
?t=<counter>) would change the URL on every load, which defeats the ETag/304 live-edit caching the R0 rails deliberately depend on (plugins/__init__.py)
- so the token needs to be applied only when re-loading an already-evaluated URL, not on first load
That interaction wants its own PR and an on-device test (install plugin vX → upgrade to vY → roll back to vX → confirm the screen actually re-renders).
Repro sketch
- Install a
scriptType: "module" plugin at v1.0.0, load the app, open its screen.
- Update it to v1.1.0 (screen re-loads correctly — new
?v=).
- Roll back to v1.0.0 without reloading the page.
- The v1.0.0 module is already in the module map → not re-evaluated → the screen keeps showing v1.1.0's state.
Found by CodeRabbit during the R3a plugin-loader carve (#878). Pre-existing — it arrived with the R0 module rails (#812) and was moved verbatim, so it is not a regression from the carve.
The bug
static/js/plugin-loader.jsbuilds the plugin script URL from the plugin's version:ES modules are evaluated once per URL per document. Re-inserting a
<script type="module">whosesrcis already in the module map does not re-execute it.So on a downgrade — rolling a plugin back to a version already evaluated this session — the URL is one the module map has seen, the script silently does not re-run, and
loadedScriptsrecords it as loaded anyway. The plugin screen stays stale.Upgrades are fine (a new version yields a new URL). It is specifically the rollback / re-install-older path. Classic-script plugins are unaffected — only
scriptType: "module"plugins.Why it wasn't fixed in #878
That PR is a verbatim carve whose whole value is being provably behaviour-neutral. The fix is a real behaviour change to the plugin-loading path, and it is not a one-liner:
?t=<counter>) would change the URL on every load, which defeats the ETag/304 live-edit caching the R0 rails deliberately depend on (plugins/__init__.py)That interaction wants its own PR and an on-device test (install plugin vX → upgrade to vY → roll back to vX → confirm the screen actually re-renders).
Repro sketch
scriptType: "module"plugin at v1.0.0, load the app, open its screen.?v=).