Skip to content

release: v1.12.0#236

Merged
TemujinCalidius merged 4 commits into
mainfrom
dev
Jul 12, 2026
Merged

release: v1.12.0#236
TemujinCalidius merged 4 commits into
mainfrom
dev

Conversation

@TemujinCalidius

Copy link
Copy Markdown
Owner

Release v1.12.0 — dev → main (merge commit). Ships the crosspost reliability work that soaked on dev since v1.11.0.

Highlights

  • Failed crossposts are retried automatically instead of being lost (Compose-path Bluesky crosspost failures are silent: success:false discarded unlogged (scheduled path already logs+retries) #225). A Bluesky/Threads crosspost that failed transiently at publish time (network blip, 5xx, HTTP/2 GOAWAY) used to be discarded with no log line and no retry — a real incident lost a video post's Bluesky copy with zero evidence. Now every failure is logged, and Bluesky/Threads failures are persisted and retried by the scheduler with backoff (2m → 24h), writing the blueskyUri/threadsPostId marker on success. A re-read-before-repost guard prevents duplicate public posts (Bluesky/Threads don't dedupe like ActivityPub). New fourth toggle on /admin/settings.
  • Dependency: @atproto/api 0.20.26 → 0.20.27.

Schema (operators)

Additive only — new FailedCrosspost table. npm run update applies the migration. No breaking changes.

Full notes: the ## 1.12.0 (2026-07-12) section of CHANGELOG.md.

TemujinCalidius and others added 4 commits July 8, 2026 18:00
…ding them (#225) (#231)

crosspostToBluesky/Threads/DayOne return { success:false } on a transient
failure (they never throw), but the compose path only handled the Bluesky
success case and used a bare .catch() for Threads/DayOne — which only sees
thrown errors. So a Bluesky/Threads/Day One blip at publish time silently
lost the crosspost with no log line (an incident lost a video's Bluesky copy
this way with zero evidence).

- Bluesky: add an `else if (!success)` branch logging the returned error
  (matches publish-post.ts / the scheduled path).
- Threads + DayOne: check the returned `success` in a .then() and log the
  error, keeping the .catch() for thrown errors.

Makes the failures visible; automatic retry lands separately (#225 part 2).
Test drives the create path with each crosspost failing.

Co-authored-by: Samuel Lison <samuel@slcolonies.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
) (#232)

* feat(crosspost): persist + retry failed Bluesky/Threads crossposts (#225)

Part 2 of #225 (the robustness fix, building on the logging in #231). A
transient crosspost failure at compose time was logged but still lost — the
crosspost was never re-attempted.

- FailedCrosspost table (new; @unique(postId,platform) + nextRetryAt index)
  + manual migration. Stores the exact crosspost args (payload JSON) re-sent
  verbatim on retry.
- compose/route.ts enqueues a Bluesky/Threads failure (Day One, a local
  journal export, logs only — no retry).
- src/lib/crosspost-retry.ts retryFailedCrossposts(): claims each due row via
  an atomic compare-and-swap on nextRetryAt, re-attempts via
  crosspostToBluesky/crosspostReplyToBluesky/crosspostToThreads, writes the
  blueskyUri/threadsPostId marker + deletes on success, backoff
  (2m→10m→1h→6h→24h) then terminal after 6 attempts, prunes ONLY terminal
  rows by failedAt age (never a pending row — the #207 prune lesson).
- Fourth scheduler job crosspostRetry (SCHEDULER_CROSSPOST_*, default on/60s)
  wired into the config overlay + the live /admin/settings toggle.

Tests: 413 total (crosspost-retry: enqueue payload/idempotency, per-platform
re-send + marker write, atomic claim, backoff steps + give-up boundary both
sides, terminal-only prune; scheduler dispatch; compose enqueues on failure).

Closes #225

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(crosspost): duplicate-guard the retry (no remote dedupe) + clean up on delete

Adversarial review: unlike AP redelivery (deduped remotely by stable id),
Bluesky/Threads don't dedupe a re-sent post — so a crash between a successful
crosspost and its bookkeeping, or a landed-but-reported-false original, could
publish a DUPLICATE public post.

- retryFailedCrossposts now re-reads the Post before re-posting and SKIPS
  (deleting the row) if the platform marker is already set or the post is gone;
  a DB read error skips the tick (keeps the row) rather than risk a duplicate.
- deletePostWithFederation also clears FailedCrosspost rows, so deleting a post
  can't leave a pending retry that publishes the removed content later.
- Documented the irreducible residual (landed-but-reported-false → one possible
  duplicate; the platforms offer no client idempotency key).
- Tests: marker-set skip, deleted-post skip, unverifiable-skip-keeps-row,
  claim writes a 5-min lease; delete-post asserts the FailedCrosspost cleanup.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Samuel Lison <samuel@slcolonies.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Patch bump, triage sandbox-verified + re-verified here (tsc clean, 416 tests,
build clean). No code changes needed.

Co-authored-by: Samuel Lison <samuel@slcolonies.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
prepare-release.mjs minor: '## Unreleased' → '## 1.12.0 (2026-07-12)', package.json + lock bumped. Ships the #225 crosspost silent-failure fix (log + persisted retry queue) + @atproto/api 0.20.27.

Co-authored-by: Samuel Lison <samuel@slcolonies.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@TemujinCalidius
TemujinCalidius merged commit 1e14656 into main Jul 12, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant