You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
RoomListDisplay::Migrating is reachable only when ROOMS is empty andROOMS_LOAD_STATE == Migrating, which only the legacy-delegate migration paths set. Those run when the CURRENT delegate's key index holds no room:<vk> slots (LoadPlan::ProbeLegacy). So the notification recurs exactly when the migration never manages to write the rooms to the current delegate.
Cause 1 — the legacy hoard exhausts the hosted per-user quota
River copies the whole room set into each new delegate generation and never deletes the old one ("left in place as a rollback fallback"). legacy_delegates.toml is at 27 generations.
The hosted node (--hosted-mode, try.freenet.org) enforces freenet-core's DEFAULT_PER_USER_SECRET_QUOTA_BYTES (4 MiB) summed across every delegate the user has secrets under. So a long-standing user's quota is consumed by their own stale duplicates → the migration's re-save to the current delegate is refused → the rooms stay only on a legacy generation → the next visit migrates from scratch again.
Measured on the production node (/home/ian/freenet-hosted/data/secrets), one user at 3,998.5 KiB of 4 MiB — 1,602 bytes of headroom:
room slot
rank 21
22
23
24
25
26
27 (current)
GfxrVa…
447 KB
454 KB
459 KB
1.16 MB
–
absent
2vkPhG… (official room)
181 KB
186 KB
186 KB
184 KB
437 KB
–
absent
7xCWkX…
86 KB
87 KB
87 KB
87 KB
88 KB
–
absent
DsjVrC…
7 KB
7 KB
7 KB
7 KB
7 KB
7 KB
absent
The current delegate holds only the 73-byte signing keys, rooms_meta and a 94-byte key index — not one room slot. ~2.5 MB of the quota is pure duplication of the same rooms across generations 21–25.
Cause 2 — the migration seal can never persist in the deployed app
is_legacy_migration_done() / mark_legacy_migration_done() / is_legacy_migration_in_progress() use localStorage. The gateway serves the app in sandbox="allow-scripts allow-forms allow-popups …" with no allow-same-origin, so the document has an opaque origin. Measured live against try.freenet.org:
origin: "null"
localStorage -> THROWS: SecurityError: Failed to read the 'localStorage' property
from 'Window': The document is sandboxed and lacks the 'allow-same-origin' flag.
window.local_storage() maps that to Err, which the readers silently treat as "flag absent". Consequences, all verified:
the seal is never written and always reads false, so the legacy fan-out (27 generations × {rooms_data GET, outbound_dms GET, ListRequest} = 81 delegate requests) fires on every page load for any user in ProbeLegacy state. Observed in-browser: 27 probes dispatched, all 27 answered.
node cost: delegates.rs:433 ("Delegate not found in store (expected for migration probes)") is rate-limited at 30/s and still drops 300–600 events/hour on the try.freenet.org node.
Fix
Persist both migration flags in the chat delegate itself — the only durable store the app has in that environment — read back from the current delegate's existing startup ListResponse (no extra round trip).
Prune superseded copies: for each key, keep only the highest-ranked legacy generation that holds it and delete strictly lower-ranked duplicates. Per Lost IDs and rooms to River Update across two nodes. #527 generation authority the lower copies are already unreachable state, so this can never remove a last copy.
Known remaining cost (not this issue)
A user with genuinely no rooms anywhere never reaches a completion point that marks migration done, so they still pay the 81-request fan-out on every load. Sealing that case is deliberately conservative (the empty-rooms_data branch must not mark done — see the V24→V25 data-loss pin), so it is left alone here and tracked separately.
Symptom
Every visit to https://try.freenet.org/v1/contract/web/raAqMhMG7KUpXBU2SxgCQ3Vh4PYjttxdSWd9ftV7RLv/ shows the "Migrating your rooms…" state, even when nothing has been upgraded since the previous visit.
Why it recurs
RoomListDisplay::Migratingis reachable only whenROOMSis empty andROOMS_LOAD_STATE == Migrating, which only the legacy-delegate migration paths set. Those run when the CURRENT delegate's key index holds noroom:<vk>slots (LoadPlan::ProbeLegacy). So the notification recurs exactly when the migration never manages to write the rooms to the current delegate.Cause 1 — the legacy hoard exhausts the hosted per-user quota
River copies the whole room set into each new delegate generation and never deletes the old one ("left in place as a rollback fallback").
legacy_delegates.tomlis at 27 generations.The hosted node (
--hosted-mode, try.freenet.org) enforces freenet-core'sDEFAULT_PER_USER_SECRET_QUOTA_BYTES(4 MiB) summed across every delegate the user has secrets under. So a long-standing user's quota is consumed by their own stale duplicates → the migration's re-save to the current delegate is refused → the rooms stay only on a legacy generation → the next visit migrates from scratch again.Measured on the production node (
/home/ian/freenet-hosted/data/secrets), one user at 3,998.5 KiB of 4 MiB — 1,602 bytes of headroom:GfxrVa…2vkPhG…(official room)7xCWkX…DsjVrC…The current delegate holds only the 73-byte signing keys,
rooms_metaand a 94-byte key index — not one room slot. ~2.5 MB of the quota is pure duplication of the same rooms across generations 21–25.Cause 2 — the migration seal can never persist in the deployed app
is_legacy_migration_done()/mark_legacy_migration_done()/is_legacy_migration_in_progress()uselocalStorage. The gateway serves the app insandbox="allow-scripts allow-forms allow-popups …"with noallow-same-origin, so the document has an opaque origin. Measured live against try.freenet.org:window.local_storage()maps that toErr, which the readers silently treat as "flag absent". Consequences, all verified:false, so the legacy fan-out (27 generations × {rooms_dataGET,outbound_dmsGET,ListRequest} = 81 delegate requests) fires on every page load for any user inProbeLegacystate. Observed in-browser: 27 probes dispatched, all 27 answered.rooms_datais a blind full-blob overwrite (last-write-wins across tabs) #345 interrupted-migration recovery is dead code in production (is_legacy_migration_in_progress()is permanently false).delegates.rs:433("Delegate not found in store (expected for migration probes)") is rate-limited at 30/s and still drops 300–600 events/hour on the try.freenet.org node.Fix
ListResponse(no extra round trip).Known remaining cost (not this issue)
A user with genuinely no rooms anywhere never reaches a completion point that marks migration done, so they still pay the 81-request fan-out on every load. Sealing that case is deliberately conservative (the empty-
rooms_databranch must not mark done — see the V24→V25 data-loss pin), so it is left alone here and tracked separately.[AI-assisted - Claude]