| … | … | @@ -0,0 +1,173 @@ |
| 1 | # Holding and Telling |
| 2 | |
| 3 | **Status:** design. Mechanism verified in shipped code; policy undecided. |
| 4 | **Date:** 2026-09-08 |
| 5 | **Sits under:** `SOCIAL-MODEL.md` (the room, the record, visibility as a place). |
| 6 | **Answers two questions:** who holds a copy of what, and who gets told about it. |
| 7 | |
| 8 | --- |
| 9 | |
| 10 | ## 0. The outage that produced this |
| 11 | |
| 12 | 2026-09-08, measured. Tabertron held five pointers from ashatron's feed and could read |
| 13 | none of them. Ashatron — a laptop — had gone offline, and it was the only machine holding the |
| 14 | bytes. The surface said so honestly: *"5 record(s) could not be read — the log arrived, the bytes |
| 15 | did not."* When ashatron came back, all five resolved. |
| 16 | |
| 17 | Nothing was broken. Every layer did what it was built to do. **The bytes were only ever fetched |
| 18 | when somebody tried to read them**, and nobody had, so no second copy existed. |
| 19 | |
| 20 | ## 1. Every post is two things |
| 21 | |
| 22 | A **note** — "this post exists, here is its fingerprint" — and the **text** itself. |
| 23 | |
| 24 | Notes travel automatically: they live in the log, and the log replicates to anyone holding the |
| 25 | feed. Text does not travel automatically. A machine fetches text by fingerprint, on demand, when a |
| 26 | reader asks for it. |
| 27 | |
| 28 | Like a library. You receive everyone's catalogue cards automatically, and you order a book only |
| 29 | when somebody asks to read it. If the library shuts while you are holding only cards, you are |
| 30 | holding nothing you can read. |
| 31 | |
| 32 | ## 2. The one idea |
| 33 | |
| 34 | **Everything is pull. The only thing that is pushed is a doorbell ring.** |
| 35 | |
| 36 | Pull is what makes the fingerprint a capability: you can only fetch bytes you can name, and you can |
| 37 | only name them if a pointer reached you, which membership already governs. Push inverts that — it |
| 38 | lets a stranger decide what lands on your disk. |
| 39 | |
| 40 | ⛔ **`autoSync` is not the lever.** hyper-overlay carries an older path-based mode where peers |
| 41 | broadcast their file lists and push files at each other. It is off by construction and must stay |
| 42 | off: the overlay attaches to *every* swarm connection regardless of which feed justified it, so a |
| 43 | path-based offer crosses every scope boundary the room and follow model exists to maintain. |
| 44 | |
| 45 | ## 3. Three tiers, one mechanism |
| 46 | |
| 47 | The tiers differ only in **how long you keep what you pulled**. |
| 48 | |
| 49 | | | you pull | you keep it | you serve it on | |
| 50 | |---|---|---|---| |
| 51 | | **browse** (peek) | one profile, on demand | while you are looking | yes — this is the network-health case | |
| 52 | | **follow** | everything, on arrival | durably | yes | |
| 53 | | **interaction** | one record, because you were told | while the thread matters | yes | |
| 54 | |
| 55 | ★ **Serving on is free and already automatic.** hyper-overlay re-seeds by default: a node that |
| 56 | fetches a blob registers it and becomes a source for it. *"A file held by N peers then gets faster |
| 57 | as more peers fetch it."* So browsing is not merely tolerated — several people reading the same |
| 58 | profile is how that profile stays available. |
| 59 | |
| 60 | ★ **The browse/follow line already exists in code.** `timeline` skips peeked feeds deliberately: |
| 61 | putting them in *"would make reading a stranger's page indistinguishable from following them, |
| 62 | which is the whole thing peek exists to avoid."* This document makes that distinction mean |
| 63 | something about storage too. |
| 64 | |
| 65 | ⇒ **Following becomes a storage decision, not only a display one.** To follow is to say: keep their |
| 66 | bytes, durably, and serve them to others. That is the blind-mirror the outage in §0 needed. |
| 67 | |
| 68 | ## 4. Mirror mode already exists |
| 69 | |
| 70 | Verified in `hyper-overlay` 0.3.13, unwired in favela-peer. |
| 71 | |
| 72 | - **Fetching:** `fetchFile` takes a `mirrorSpool` option that stores the verified blob |
| 73 | content-addressed at `<spool>/<hash>` — *"the mirror path — a node with no working tree of its own |
| 74 | fetches files into a content-addressed spool so it durably re-serves them."* |
| 75 | - **Serving:** `syncBaseDirs` makes the serve path resolve a request straight from that spool — |
| 76 | *"a pure function of the hash + dir, like a git object store. This is what lets a node keep serving |
| 77 | its spooled files across restarts with no rehydration step."* |
| 78 | |
| 79 | ★ The spool needs no boot-time repair. The existing serve map is in-memory and has to be rebuilt at |
| 80 | startup — a gap that once cost 75+ silent hours — because it maps paths. The spool derives its path |
| 81 | from the hash, so a restart loses nothing. |
| 82 | |
| 83 | **What is missing is only the trigger.** Nothing fetches on arrival; the fetch happens when a reader |
| 84 | asks. Wiring is: pass a spool, pass `syncBaseDirs`, and fetch in the background when a followed |
| 85 | feed's log gains entries, with a cap on concurrent files. |
| 86 | |
| 87 | ⚠ Per-file concurrency is already solved and should not be re-implemented: the chunk scheduler holds |
| 88 | a dynamic RTT-scaled inflight window per peer, modelled on hypercore's replicator, with stall |
| 89 | hotswap and reassignment on disconnect. What is unbounded is the number of *files* in flight. |
| 90 | |
| 91 | ## 5. The doorbell — the half that does not exist |
| 92 | |
| 93 | An interaction is written in its author's own feed. A like on your post is a record in **my** diary. |
| 94 | You learn of it only if you happen to hold my feed. |
| 95 | |
| 96 | ⛔ **So an interaction from someone who does not follow you never arrives.** Not late — never. That |
| 97 | is true today of likes, replies and mentions alike. The claim that interactions need no delivery |
| 98 | holds only between people who already hold each other's feeds, and the peer's own notes admit the |
| 99 | remainder: *"what is genuinely left is a stranger you hold nothing of."* |
| 100 | |
| 101 | There is one working example of the shape: `knock`, the doorbell, *"the ONE op a not-yet-member can |
| 102 | usefully send."* It is bounded structurally — keyed by feed, so a flood is one row, not thousands — |
| 103 | which matters because a rate limit inside apply would make the view non-deterministic. |
| 104 | |
| 105 | **What is pushed is a pointer, never content.** "There is a reply to your post, over here." Small, |
| 106 | countable, bounded. You then fetch the reply itself — which is browsing, one record. |
| 107 | |
| 108 | ## 6. A reply is a post that rings a doorbell |
| 109 | |
| 110 | A reply does not live in two places. It lives in its author's feed like any other post; only the |
| 111 | announcement travels. |
| 112 | |
| 113 | - **A post:** I wrote this. Anyone holding my feed sees it. |
| 114 | - **A reply:** I wrote this *and it points at you*. Anyone holding my feed sees it, **and you need |
| 115 | telling**, because you probably do not hold my feed. |
| 116 | |
| 117 | The code already says this — *"A reply IS a post that names a target"* — and this is what reconciles |
| 118 | it with being a post. Storage is uniform; **reach is what varies**. |
| 119 | |
| 120 | ## 7. Announcing a post is not, by itself, an interaction |
| 121 | |
| 122 | - **To your followers:** their subscription already carries it. Following means their machine |
| 123 | replicates your log; a new post is a new log entry. An announcement on top would duplicate it. |
| 124 | - **To anyone else:** it is an interaction only if the post *names* them — a mention, a reply, a |
| 125 | quote. If it does not name them, an unsolicited announcement is what the network exists to refuse. |
| 126 | |
| 127 | ⇒ **Following is a standing subscription; the doorbell is one-off and addressed.** "Tell me |
| 128 | everything you write" versus "this particular thing concerns you." |
| 129 | |
| 130 | ⚠ **Waking a sleeping device is a third thing** and must not be folded into either. Whether the |
| 131 | doorbell rang is a data question; whether a phone lights up is a transport question. The peer's own |
| 132 | notes already draw this line: *"waking a sleeping device, which is a separate job entirely."* |
| 133 | |
| 134 | ## 8. What this changes elsewhere |
| 135 | |
| 136 | ★ **"Notification" stops being one large unsolved item.** It has been carried as the biggest open |
| 137 | thing for four sessions. It is two: |
| 138 | |
| 139 | - from people you follow — **already works**, a projection over feeds you hold |
| 140 | - from people you do not — **the doorbell**, small and specific |
| 141 | |
| 142 | ★ **The doorbell and the fediverse inbound problem are the same problem.** Inbound ActivityPub |
| 143 | activity currently has nowhere to go in the peer, because its author has no feed — so it is merged |
| 144 | at the view layer as a second source. The doorbell is that place. With it, a reply from Mastodon and |
| 145 | a reply from a favela stranger stop being two mechanisms. |
| 146 | |
| 147 | ## 9. Open |
| 148 | |
| 149 | 1. **Bounding the doorbell.** `knock` gets away with one row per feed because it is a single |
| 150 | yes/no. Likes and replies are per-post and per-person, so "one row" does not hold, and unbounded |
| 151 | means anyone may write into your feed for ever. Bounding by *relationship* is the likely shape — |
| 152 | whose ring is accepted is a choice, as following is. |
| 153 | 2. **Retention per tier.** §3 names three lifetimes and nothing implements any of them. Mirror mode |
| 154 | without retention is a disk-filler; `evictContent` and `compact` exist and nothing calls them. |
| 155 | 3. **Which feeds mirror.** All followed, or own-devices-only? Own-devices is bounded by construction |
| 156 | and fixes §0 on its own. |
| 157 | 4. **Does browsing spool or scratch?** A peek that spools becomes a durable mirror by accident. |
| 158 | |
| 159 | ## 10. What is measured and what is decided |
| 160 | |
| 161 | Following `SOCIAL-MODEL.md` §2.5's rule — a claim about the lower layers must cite a measurement. |
| 162 | |
| 163 | | | claim | status | |
| 164 | |---|---|---| |
| 165 | | §0 | five records unreadable, resolved when the author returned | ✅ measured | |
| 166 | | §3 | re-seed on fetch is on by default | ✅ read in source | |
| 167 | | §3 | `timeline` excludes peeked feeds | ✅ read in source | |
| 168 | | §4 | `mirrorSpool` + `syncBaseDirs` exist and are unwired here | ✅ read in source | |
| 169 | | §4 | the chunk scheduler bounds per-peer inflight | ✅ read in source | |
| 170 | | §5 | an interaction from a non-follower never arrives | ✅ read in source | |
| 171 | | §2 | pull is the security model, push is not | ⚠ decided | |
| 172 | | §3 | follow means mirror durably | ⚠ decided | |
| 173 | | §6 | a reply is a post plus a ring | ⚠ decided | |