| … | … | @@ -9,7 +9,7 @@ updated: 2026-08-05 |
| 9 | 9 | |
| 10 | 10 | # ap-bridge |
| 11 | 11 | |
| 12 | | **v0.0.1** (M0→M3 shipped 2026-08-01), runtime **Bare**, pgh app `ap-bridge`, |
| 12 | **v0.0.4** (M0→M3 shipped 2026-08-01; control surface + OTA 0.0.2, `reproject` 0.0.3, `updated` fix 0.0.4), runtime **Bare**, pgh app `ap-bridge`, |
| 13 | 13 | port **3400**, live at `https://ap.7rnx.net` (vhost → stortron:3400). |
| 14 | 14 | 70 tests / 254 asserts, all offline. |
| 15 | 15 | |
| … | … | @@ -106,7 +106,7 @@ ap-bridge its own hyper-overlay reader. It needs none — records live in the sh |
| 106 | 106 | `OverlayRepoStore` and `atproto-did-peer` already reads through it and carries the |
| 107 | 107 | blob relay. The device reduces to **hyper-proto-peer + one RPC (`sign-http-request`)**. |
| 108 | 108 | |
| 109 | | ## Delivery — the two traps |
| 109 | ## Delivery — the three traps |
| 110 | 110 | |
| 111 | 111 | `lib/publisher.js`. Mastodon does **not** read an actor's outbox for timelines and |
| 112 | 112 | does **not** backfill on follow. A correct, public, fetchable outbox puts *nothing* |
| … | … | @@ -134,7 +134,63 @@ projection improvement reaches already-delivered posts automatically. |
| 134 | 134 | and (b) a deploy that improved the projection booted clean and sent nothing. Both |
| 135 | 135 | needed an explicit kick. **An event-driven system needs a defined boot-time |
| 136 | 136 | reconciliation, or state changes that aren't events are invisible.** |
| 137 | | `AP_BACKFILL=N` is one-shot by construction and self-disarms. |
| 137 | `AP_BACKFILL=N` is one-shot by construction and self-disarms. **`reproject` (0.0.3) is |
| 138 | that reconciliation as an operator verb** — see below. |
| 139 | |
| 140 | ## ⛔ An `Update` WITHOUT `updated` is silently discarded (0.0.4) |
| 141 | |
| 142 | **The trap that makes every other Update mechanism above a no-op.** Mastodon accepts the |
| 143 | POST, returns **2xx**, and does **nothing**: no edit, no attachment re-fetch, `edited_at` |
| 144 | stays `null`. Nothing anywhere reports a failure. |
| 145 | |
| 146 | Required since **Mastodon 3.5.0**, and it is an *unofficial* requirement — documented in |
| 147 | community write-ups rather than the spec, so you will not find it by reading ActivityPub. |
| 148 | |
| 149 | Measured end-to-end 2026-08-05, and this is the shape to recognise: |
| 150 | |
| 151 | ``` |
| 152 | reproject {rkey, force:true} -> {"ok":true,"sent":1,"updates":1} |
| 153 | log -> {"msg":"published","verb":"Update", ...} |
| 154 | prsm.space delivery -> 2xx, no error |
| 155 | prsm.space status -> media_attachments: 0, edited_at: null ← nothing happened |
| 156 | ``` |
| 157 | |
| 158 | 0.0.3 shipped `reproject` with green tests, a happy RPC, a clean log and a 2xx — and it |
| 159 | changed nothing. **Only reading the actual Mastodon status caught it.** Third instance this |
| 160 | session of the class #24 and #26 both name: *everything we control reports success*. |
| 161 | |
| 162 | The fix (`publisher.js`, in `_send`): |
| 163 | |
| 164 | ```js |
| 165 | const object = verb === 'Update' ? { ...note, updated: new Date().toISOString() } : note |
| 166 | ``` |
| 167 | |
| 168 | Two properties that are load-bearing and are pinned by tests: |
| 169 | - **Set at SEND time, never in the projection.** If `updated` were part of the note the |
| 170 | fingerprint would change on every drain and every post would look permanently edited. |
| 171 | - **A `Create` never carries it.** A first delivery must not claim to be an edit. |
| 172 | |
| 173 | ## `reproject` — the operator verb (0.0.3) |
| 174 | |
| 175 | Over the control socket (`~/.config/ap-bridge/svc.sock`, 0.0.2+): |
| 176 | |
| 177 | | call | effect | |
| 178 | |---|---| |
| 179 | | `reproject` | re-derive all; `Update` whatever CHANGED | |
| 180 | | `reproject {rkey}` | one post | |
| 181 | | `reproject {force:true}` | ignore fingerprints, re-send regardless | |
| 182 | | `reproject {verify:false}` | override the media guard | |
| 183 | |
| 184 | ★ **`force` is usually what you want, and the reason is counter-intuitive.** Posts whose |
| 185 | media never rendered on Mastodon typically have an **unchanged** fingerprint — they were |
| 186 | delivered *with* the attachment and Mastodon simply failed to fetch the bytes at the time |
| 187 | and never retries. So the default (changed-only) path correctly does nothing, and `force` |
| 188 | is the tool, not the fallback. Measured: 5 such posts, all fixed by per-rkey `force`. |
| 189 | |
| 190 | ⛔ **The media guard is not a nicety.** `reproject` probes each attachment against our own |
| 191 | PDS and SKIPS a post whose media is unservable. One post (`3mpquxgrvu227`, 2026-07-03) has |
| 192 | blob bytes lost before the overlay migration; re-sending it teaches the receiver the same |
| 193 | 404 again AND burns the one edit that would have fixed it had the bytes ever returned. |
| 138 | 194 | |
| 139 | 195 | ## Blobs — why proxy instead of linking the membrane |
| 140 | 196 | |
| … | … | @@ -218,8 +274,11 @@ handlers **without** `swarm.join(discoveryTopic(did))` — nothing ever connects |
| 218 | 274 | |
| 219 | 275 | ## Open work |
| 220 | 276 | |
| 277 | - ~~exit-75 / `release-ready` handling~~ — ✅ **0.0.2**. Control surface at |
| 278 | `~/.config/ap-bridge/svc.sock` + sidecar registration; the sidecar installs, calls |
| 279 | `release-ready`, we answer FIRST then exit 75. Two hands-off deploys verified. |
| 280 | Deliberately NOT pgh-runtime (root trees without dep closures = broken install). |
| 221 | 281 | - `sign-http-request` on hpp → genuinely keyless bridge (M2's remaining piece) |
| 222 | | - exit-75 / `release-ready` handling → end the manual stop/install/start cycle |
| 223 | 282 | - facets → link/mention markup; quotes (FEP-044f); `Delete`/`Tombstone` |
| 224 | 283 | - multi-actor: ★ the id-map must become **real state** first — it is a pure function |
| 225 | 284 | only while there is one DID and one collection |