OverviewFilesCommitsIssuesDocsReleasesPeers

favela-web commits / d27fc91

0.3.85: a stale copy is not a co-hosted one, so gaps can close
Reported from a handset: ashatron's avatar wrong and posts two weeks old. One fault, and this is its cause. let coHosted = false try { const s = await getRecordStore(did); coHosted = (await s.records()).length > 0 } catch {} coHosted decides the ingest baseline: true tails from the CURRENT END (right for a DID whose records hpp holds authoritatively), false rebuilds from the firehose. But it was computed from "are there any records on disk", and the backfill gate ten lines below already names why that cannot work — "it is self-fulfilling, because our own putPost populates that store for ingested peers too". So the first glance at a DID gave it records, which made it co-hosted for ever, which meant every later boot tailed from the end. A copy that started incomplete stayed incomplete permanently. Measured: ashatron indexed once in August, newest post 2026-08-09 on the phone against 2026-08-23 on stortron, and the avatar still the old CID because the profile record updated inside the hole. ⛔ The failure mode is the damage. A DID nobody is filling in does not render as absent or stale — it renders as a complete, plausible profile that is simply months out of date. It reads as corruption, and that is how it was reported. coHosted now means what its own comment always said: hpp holds this DID. Read from HOSTED_DIDS, the same authority /api/track consults HOSTED-FIRST for the same reason. Hosted DIDs are unaffected. Everything else rebuilds from the firehose, which is the only copy that can close a gap. Incidentally removes an O(repo) read — the old predicate loaded every record of a DID to compute a boolean, once per peer added. ⚠ KNOWN COST: non-hosted DIDs now replay from seq 0 on every boot. It is idempotent (putPost/putProfile dedup by content signature, so a replay re-reads without re-writing) but it re-reads frames each launch, on a phone. The real fix is a persisted per-DID fold cursor so a boot resumes where it stopped; the marker that exists today (needsBackfill) is a per-build boolean and cannot express a position. Until then, correct-and-costly beats cheap-and-frozen. Suite green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
d27fc91ccc20cb65742e2980dc9927373dee3ff6
by plankatron <fleeky@gmail.com> · 2026-08-25T18:28:27.000Z

Changed files (2) +20 -3

bin/favela-web.js +19 -2
……@@ -653,8 +653,25 @@ async function addPeer (did) {
653653 // the BASELINE source: a DID whose records hpp already holds on this box (wj62/loser-club on tabertron)
654654 // reads them from the AUTHORITATIVE shared store (bypasses the membrane fork/desync); a non-co-hosted DID
655655 // rebuilds its repo from the FIREHOSE BACKFILL (subscribeRepos(0) — each #commit frame carries the bytes).
656 let coHosted = false
657 try { const s = await getRecordStore(did); coHosted = s ? (await s.records()).length > 0 : false } catch {}
656 // ⛔ CO-HOSTED MEANS hpp HOLDS IT, NOT "WE HAVE SOME RECORDS FOR IT". This was
657 // `(await store.records()).length > 0`, which is SELF-FULFILLING — the gate below says so in its own
658 // comment: our own putPost populates the shared store for ingested peers too. So the first time we
659 // glanced at a DID it acquired records, became "co-hosted" for ever, and from then on every boot
660 // tailed from the CURRENT END and could never fill a gap.
661 //
662 // Measured on a handset (hyper-social #53): ashatron viewed once in August, then frozen — posts 16
663 // days stale and the PREVIOUS avatar rendering as current, because the profile record was inside the
664 // hole. It reads as corruption rather than as a sync gap, which is the worst way for it to fail.
665 //
666 // hpp's config is the authority (the same source /api/track calls HOSTED-FIRST for the same reason).
667 // A DID we actually host is authoritative on disk and rightly tails from the end; anything else
668 // rebuilds from the firehose, which is the only copy that can close a gap.
669 //
670 // ⚠ Non-hosted DIDs therefore replay from seq 0 on each boot. That is idempotent (putPost/putProfile
671 // dedup by content signature, so a replay re-reads without re-writing) but it re-reads frames every
672 // launch. The real answer is a persisted per-DID fold cursor so a boot resumes instead of replaying;
673 // until that exists, correct-and-costly beats cheap-and-frozen.
674 const coHosted = (HOSTED_DIDS || []).includes(did)
658675 console.log(JSON.stringify({ msg: 'firehose-only (noRepo) DID', did, baseline: coHosted ? 'shared-store' : 'firehose-backfill' }))
659676 const peer = router.add(did, { verifyKey, noRepo: true, onError: (e) => console.error('[peer]', did, e.message) })
660677 byDid.set(did, peer); peers.push(peer)
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "favela-web",
3 "version": "0.3.84",
3 "version": "0.3.85",
44 "description": "favela-web — the web/SSR surface of Favela, the native P2P-local, community-owned social client + appview over the self-custody atproto stack (hyper-proto-peer). One of the Favela surfaces (alongside favela-cli and favela-native). bsky is an optional POSSE export, not the home. See DESIGN.md.",
55 "type": "module",
66 "main": "bin/favela-web.js",