feat(activitypub,mastodon-api): thread replies and hydrate boosts for local posts - #424
Conversation
… local posts Resolve two Mastodon read-surface fidelity gaps against posts the actor DO already holds, via a new pure-SQL #resolveLocalObject (owner outbox then inbox, never an outbound fetch): - A reply whose inReplyTo names a locally-held post now carries that post's snowflake as in_reply_to_id and its author as in_reply_to_account_id (the owner account for a reply to the owner's own post). - A bare-IRI Announce of a locally-held post now hydrates its reblog with the real content and author instead of rendering content-less. Targets the DO does not hold still degrade to null / content-less as before; dereferencing a remote object is the remaining increment. New optional BackendEntry.inReplyTo/boost fields thread the resolution through the adapter into statusEntity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XT45PcE3pgXgsQffQDWdhW
|
The The repo's own gates are green on this head — Generated by Claude Code |
davidwkeith
left a comment
There was a problem hiding this comment.
Reviewed the diff, spec/packages/mastodon-api.md, and the new tests. The #resolveLocalObject SQL and the DO-level plumbing (object.ts, mastodon-api.ts) look solid and consistent with existing patterns in the file (verify_state IS NOT 'failed' AND removed_at IS NULL, this.#config!, etc.), and it's applied uniformly to both the timeline and notifications reads plus the single-entry endpoint. Spec and changeset are updated appropriately.
One correctness issue worth fixing before merge (inline comment below): the new ownerAccount-dependent branch in statusEntity for in_reply_to_account_id (and the equivalent for a boosted owner post) isn't reliably wired up from the three route handlers, so the id resolution silently degrades to a synthesized remote-account id in the most common real path (mention notifications). Recommend fixing the three call sites' ownerAccount gating, plus a handler-level test exercising authorIsOwner: true through notifications.ts at minimum, since that's the gap none of the current tests catch.
CONTRIBUTING.md conformance
- PR title:
feat(activitypub,mastodon-api): thread replies and hydrate boosts for local posts— correct Conventional Commits form (lowercase type, comma-separated scope, no trailing period). - Commit message matches the same convention and is well-described.
- PR body uses the template's
Summary/Packages affected/Checklistheadings verbatim, with the one inapplicable item left unchecked with a reason rather than deleted. ✅. - Spec (
spec/packages/mastodon-api.md) updated in the same PR to reflect the new behavior and remaining gap. ✅. - Changeset added for both affected publishable packages (
@dwk/activitypub,@dwk/mastodon-api), minor bump. ✅. - Colocated tests added in all three touched packages'
src/*.test.ts. ✅ (see inline comment for a gap in what they cover).
No other conformance issues found.
Generated by Claude Code
… owner The statusEntity in_reply_to_account_id / reblog owner branches only resolved to the real owner id when a route handler passed ownerAccount, but the three call sites gated that on the entry's own source === 1 — so a mention notification for a reply to the owner's post (the primary reply-threading path) always fell through to a synthesized r_... id, and notifications.ts never fetched ownerAccount at all. Add entryNeedsOwnerAccount (source 1, or replies to / boosts an owner post) and gate all three handlers on it; thread ownerAccount through notificationEntity into the mention's status. Handler-level tests through notifications.ts and statuses.ts now exercise authorIsOwner: true. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XT45PcE3pgXgsQffQDWdhW
davidwkeith
left a comment
There was a problem hiding this comment.
Automated review pass.
CONTRIBUTING.md conformance: checked out the branch, ran a full repo pnpm build (needed since @dwk/mastodon-api/@dwk/activitypub consume sibling workspace packages through their dist/ exports maps), then the local CI gate scoped to both touched packages — pnpm --filter @dwk/activitypub typecheck, pnpm --filter @dwk/mastodon-api typecheck, pnpm test --project @dwk/activitypub (291/291 passing), pnpm test --project @dwk/mastodon-api (157/157 passing), plus repo-wide pnpm lint/pnpm format:check. All clean. PR title, changeset, and spec updates (spec/packages/mastodon-api.md Known-gaps section) follow the required conventions, and the unchecked catalog checklist item has a valid reason.
Correctness: traced #resolveLocalObject's two SQL paths (owner outbox, then inbox with the existing removed_at/verify_state tombstone filters — outbox has no such column, but outbox rows are hard-deleted rather than soft-tombstoned elsewhere in this file, so that's consistent) and the entryNeedsOwnerAccount wiring across statuses.ts/timelines.ts/notifications.ts. The regression the tests specifically call out (notifications.ts not threading ownerAccount into notificationEntity) is fixed and covered. Left one inline note on a minor, non-blocking fidelity gap in the boost/reply author's cached profile enrichment.
Generated by Claude Code
… cache The hydrated boost's nested reblog reused the outer entry's actorProfiles (booster + relayedBy), so the boosted post author's cached profile was never included and the reblog account rendered via the IRI-derived fallback. Thread the resolved boost/reply author IRIs through #clientActorProfiles — a free DO-local cache read (same budget as #resolveLocalObject) — so a cached profile enriches the nested account when one exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XT45PcE3pgXgsQffQDWdhW
davidwkeith
left a comment
There was a problem hiding this comment.
Overview
Closes two Mastodon read-surface fidelity gaps for locally-held targets: in_reply_to_id/in_reply_to_account_id threading, and bare-IRI Announce (boost) hydration. Both are resolved read-time via a new pure-SQL #resolveLocalObject(iri) (owner outbox, then inbox — never an outbound fetch), threaded through BackendEntry.inReplyTo/.boost into statusEntity.
Code quality / correctness
#resolveLocalObjectcorrectly checks outbox before inbox (so a reply/boost of the owner's own post resolves to the owner, not a stale inbox copy of the same object), and filtersremoved_at IS NULL AND verify_state IS NOT 'failed'on the inbox side — consistent with the existing filtering convention used elsewhere in this file (e.g. the pre-existing#clientInteractionsquery at line ~1834 uses the sameverify_state IS NOT 'failed'pattern), so this isn't introducing a new convention.entryNeedsOwnerAccountcorrectly widens the existingsource === 1check (used intimelines.ts/statuses.ts/notifications.ts) to also coverinReplyTo.authorIsOwner/boost.authorIsOwner— and thenotifications.test.tsregression test explicitly calls out that this was a review-flagged bug (reply to the owner's post previously resolving to a synthesizedr_...id instead of the real owner id). Good catch, well tested.statusEntity's boost-hydration branch recurses into itself to build the nested reblogStatusfrom the resolved object — clean reuse, and correctly keeps the outer Announce shell content-less (matching Mastodon's boost shape) while thereblogfield carries the real content/author.- One thing worth a second look, not necessarily a blocker:
#resolveLocalObjectruns ajson_extract(json, '$.object.id') = ?scan over the fulloutbox/inboxtables (no index on that expression) once per entry that needs resolution (#clientResolvedis called per row in the notifications/timeline listing loop). This mirrors the existing#clientInteractionsper-entry-scan pattern already in this file, so it's not a new class of problem introduced here — but as both patterns compound on the same hot paths, it may be worth a follow-up to add an index (or a generated column) onjson_extract(json,'$.object.id')/$.object.inReplyToif actor DOs start holding non-trivial outbox/inbox row counts, per the runtime-budget NFR.
CONTRIBUTING.md conformance
- ✅ PR title
feat(activitypub,mastodon-api): thread replies and hydrate boosts for local posts— correct Conventional Commits scope/format. - ✅ PR body keeps
Summary/Packages affected/Checklistheadings verbatim. - ✅ Changeset present (
.changeset/mastodon-reply-boost-fidelity.md), bumping both affected packages minor. - ✅
catalog.json/conformance/status.jsoncheckbox left unchecked with a one-line reason ("not applicable, no new worker and no conformance-status change") — correct, matches CONTRIBUTING's "leave unchecked with reason" rule. - ✅ Spec updated:
spec/packages/mastodon-api.md's "Known gaps" section rewritten to describe the new local-resolution behavior and the remaining remote-dereference gap. - ✅ Colocated tests: DO-level tests against real
#resolveLocalObjectSQL inmastodon-api.test.ts(reply-to-owner, orphan reply, bare-IRI boost hydration, cached-profile enrichment), entity-level tests inentities.test.ts/notifications.test.ts/statuses.test.ts. - Noted your comment on the PR already explaining the
github-advanced-securityfailure as the same recurring GitHub-side infra issue (model_not_supported) seen on #423, unrelated to this diff.
Test coverage
Thorough — covers the happy path for both reply threading and boost hydration, the "target not held locally" degrade-gracefully path, and the actor-profile enrichment side effect. No gaps found.
No blocking issues.
Generated by Claude Code
Summary
First of the follow-up Anglesite-gap increments: closes the two Mastodon read-surface fidelity gaps that off-the-shelf clients hit, for the case the actor DO can answer locally.
A new pure-SQL primitive
#resolveLocalObject(iri)looks a post up by its AS2 object IRI — owner outbox (source 1) first, then inbox (source 0) — with no outbound fetch. At read time the DO uses it to resolve each entry's cross-references, which the adapter threads into new optionalBackendEntry.inReplyTo/BackendEntry.boostfields, andstatusEntityconsumes:in_reply_to_id/in_reply_to_account_id— a reply whoseinReplyTonames a locally-held post now carries that post's snowflake and its author (the owner account when it's a reply to the owner's own post). Previously alwaysnull.Announcehydration — a boost whoseobjectis a bare IRI pointing at a locally-held post now renders its reblog with the real content and author, instead of a content-lessStatus.Targets the DO does not hold still degrade to
null(reply) or a content-less reblog (boost), exactly as before — dereferencing and caching a remote object is the remaining increment (the same network-fetch shape as actor-profile hydration), and the spec's Known-gaps section now says so.Packages affected
@dwk/activitypub, @dwk/mastodon-api
Checklist
spec/packages/and updated them ifbehaviour changed
src/*.test.ts) — entity-level in@dwk/mastodon-api, DO-level (real#resolveLocalObjectSQL) in@dwk/activitypubpnpm lint && pnpm format:check && pnpm typecheck && pnpm build && pnpm test(2,803 tests green)pnpm changeset) if this touches a publishablepackage
catalog.json/conformance/status.jsonif this adds a newmountable worker or changes conformance status — not applicable, no new worker and no conformance-status change
🤖 Generated with Claude Code
https://claude.ai/code/session_01XT45PcE3pgXgsQffQDWdhW
Generated by Claude Code