OverviewFilesCommitsIssuesDocsReleasesPeers

hypercore-kb commits / 4c9798d

skills/ap-bridge: the `updated` trap, reproject, and the shipped OTA
The finding that matters: an ActivityPub `Update` WITHOUT an `updated` timestamp on the object is SILENTLY DISCARDED by Mastodon. It accepts the POST, returns 2xx, and does nothing — no edit, no attachment re-fetch, edited_at stays null. Required since Mastodon 3.5.0 and documented only in community write-ups, so reading the ActivityPub spec will never surface it. Recorded with the exact shape to recognise, because every local surface lies: reproject returned {sent:1,updates:1}, the log said "published", delivery got a 2xx, and the status still showed media_attachments:0. Only reading the actual Mastodon status caught it — the third instance this session of the class #24 and #26 both name. Also documents `reproject` (0.0.3) and one counter-intuitive fact worth saving the next person: FORCE is usually what you want. A post whose media never rendered typically has an UNCHANGED fingerprint — it was delivered with the attachment and Mastodon simply failed to fetch the bytes and does not retry. So the default changed-only path correctly does nothing, and force is the tool rather than the fallback. Measured: 5 posts, all fixed. Plus the media guard's rationale (never re-send a post whose blobs 404 — it teaches the receiver the same absence again and burns the one edit that would have fixed it), and marks exit-75/release-ready DONE in Open work with the deliberate note that it is NOT pgh-runtime. Header bumped v0.0.1 -> v0.0.4. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4c9798d5f4d455cbbac0a2f0fcce7e73c3f402f9
by plankatron <fleeky@gmail.com> · 2026-08-05T23:53:38.000Z

Changed files (1) +63 -4

.claude/skills/ap-bridge/SKILL.md +63 -4
……@@ -9,7 +9,7 @@ updated: 2026-08-05
99
1010# ap-bridge
1111
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`,
1313port **3400**, live at `https://ap.7rnx.net` (vhost → stortron:3400).
141470 tests / 254 asserts, all offline.
1515
……@@ -106,7 +106,7 @@ ap-bridge its own hyper-overlay reader. It needs none — records live in the sh
106106`OverlayRepoStore` and `atproto-did-peer` already reads through it and carries the
107107blob relay. The device reduces to **hyper-proto-peer + one RPC (`sign-http-request`)**.
108108
109## Delivery — the two traps
109## Delivery — the three traps
110110
111111`lib/publisher.js`. Mastodon does **not** read an actor's outbox for timelines and
112112does **not** backfill on follow. A correct, public, fetchable outbox puts *nothing*
……@@ -134,7 +134,63 @@ projection improvement reaches already-delivered posts automatically.
134134and (b) a deploy that improved the projection booted clean and sent nothing. Both
135135needed an explicit kick. **An event-driven system needs a defined boot-time
136136reconciliation, 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
138that 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
143POST, returns **2xx**, and does **nothing**: no edit, no attachment re-fetch, `edited_at`
144stays `null`. Nothing anywhere reports a failure.
145
146Required since **Mastodon 3.5.0**, and it is an *unofficial* requirement — documented in
147community write-ups rather than the spec, so you will not find it by reading ActivityPub.
148
149Measured end-to-end 2026-08-05, and this is the shape to recognise:
150
151```
152reproject {rkey, force:true} -> {"ok":true,"sent":1,"updates":1}
153log -> {"msg":"published","verb":"Update", ...}
154prsm.space delivery -> 2xx, no error
155prsm.space status -> media_attachments: 0, edited_at: null ← nothing happened
156```
157
1580.0.3 shipped `reproject` with green tests, a happy RPC, a clean log and a 2xx — and it
159changed nothing. **Only reading the actual Mastodon status caught it.** Third instance this
160session of the class #24 and #26 both name: *everything we control reports success*.
161
162The fix (`publisher.js`, in `_send`):
163
164```js
165const object = verb === 'Update' ? { ...note, updated: new Date().toISOString() } : note
166```
167
168Two 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
175Over 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
185media never rendered on Mastodon typically have an **unchanged** fingerprint — they were
186delivered *with* the attachment and Mastodon simply failed to fetch the bytes at the time
187and never retries. So the default (changed-only) path correctly does nothing, and `force`
188is 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
191PDS and SKIPS a post whose media is unservable. One post (`3mpquxgrvu227`, 2026-07-03) has
192blob bytes lost before the overlay migration; re-sending it teaches the receiver the same
193404 again AND burns the one edit that would have fixed it had the bytes ever returned.
138194
139195## Blobs — why proxy instead of linking the membrane
140196
……@@ -218,8 +274,11 @@ handlers **without** `swarm.join(discoveryTopic(did))` — nothing ever connects
218274
219275## Open work
220276
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).
221281- `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
223282- facets → link/mention markup; quotes (FEP-044f); `Delete`/`Tombstone`
224283- multi-actor: ★ the id-map must become **real state** first — it is a pure function
225284 only while there is one DID and one collection