Skip to content

fix(ui): "Migrating your rooms…" shows on every visit — the legacy hoard fills the hosted per-user quota and the seal can never persist #586

Description

@sanity

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::Migrating is reachable only when ROOMS is empty and ROOMS_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.
  • the Multi-tab room loss: chat delegate rooms_data is 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).
  • 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

  1. 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).
  2. 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.

[AI-assisted - Claude]

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