* Improve asynchronous social publishing reliability
* fix: resume asynchronous social publishes
* fix: preserve publish checkpoints across retries
* fix: harden resumable publish lifecycle
* fix: clean retry resources on terminal failures
* test: cover resumable social publishing edge cases
* feat: add failed post retry command
* chore: remove retry command ai rule
* fix: require confirmation for post retries
* chore: remove ai rules index
* chore: remove ai social rule
* refactor: clarify TikTok derivative path validation
* refactor: simplify social publishing retries
* refactor: further simplify social publishing retries
* refactor: retry all failed post platforms
* style: import throwable in social retries
* refactor: decouple TikTok cleanup from image format
* refactor: extract missing publish scopes
* refactor: encapsulate missing scope failure
* fix: resume failed publishes and treat Instagram rate limits as transient
Keep TikTok/Instagram checkpoints on posts:retry so a manual retry does not
start a duplicate remote post. Classify Meta BUC 400s on Instagram status
polls as retryable via GraphError.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test: cover resume paths and transient Instagram rate limits
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: resume posts:retry only for in-flight publish failures
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: resume posts:retry via ErrorCategory instead of string lists
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: handle Instagram PUBLISHED and EXPIRED container statuses
Treat EXPIRED as a terminal server error so posts:retry starts over, and complete already-published containers without a second media_publish.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: recover published Instagram stories from /stories
Stories are not on GET /{ig-user-id}/media. Resume a PUBLISHED story container from the stories edge so we do not bind a feed post id.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test: cover Instagram EXPIRED retry and published recovery paths
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: stop guessing Instagram media ids from recent /media
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: checkpoint TikTok publish_id and keep in-flight photo derivatives
Persist publish_id right after /init/ so a crash can resume without a second publish. Keep hosted photos while that id is resumable, including token expiry on status fetch; prune only after success or a confirmed remote failure.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test: cover remaining TikTok in-flight derivative edge cases
Guard the empty publish_id prune path, account guards without a checkpoint, and video status 401 after /init/.
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor: map TikTok publish statuses with an official enum
Use PublishStatus for status/fetch values from the Content Posting API. Keep only the documented cases, including FAILED as the terminal failure.
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor: share in-flight publish checkpoint keys
Read TikTok and Instagram resume state through one helper so publishers, posts:retry, and derivative cleanup agree on the same keys.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: retry Instagram media_publish after transient Graph failures
A 500/code 2 after Meta already published left the job Failed as unknown.
Treat that as still-processing so resume can confirm PUBLISHED instead of posting again.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: resume Instagram publish after dropped Graph connections
A timeout or connection reset after Meta already published was marked unknown.
Treat it as still-processing so resume can confirm PUBLISHED instead of posting again.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
When a provider's API was down (5xx, timeout, DNS), the hourly
RefreshSocialToken job and daily VerifyWorkspaceConnections job were
treating it as "token revoked" and emailing the user to reconnect.
Bluesky going offline triggered false-positive disconnect notifications
because Bluesky access tokens are short-lived (2h) so every hourly
refresh failed during the outage.
- New PlatformUnavailableException: API unreachable / 5xx, transient.
TokenExpiredException stays for 4xx (token is provably bad).
- New TokenRefreshClient: normalizes failure semantics for OAuth
refresh HTTP calls across all providers. Takes a Platform enum so
typos fail at compile time and the user-facing label comes from
one source.
- ConnectionVerifier: all 8 refresh*Token methods route through the
new client. Hardcoded OAuth URLs (LinkedIn, YouTube) and Bluesky's
default PDS host moved into config/trypost.php alongside the
existing per-platform entries.
- RefreshSocialToken job: PlatformUnavailableException → log warning
and stop. Do NOT markAsTokenExpired, do NOT notify the user. Next
scheduled tick retries.
- VerifyWorkspaceConnections job: PlatformUnavailableException from
the inner refresh propagates and is treated as a transient skip.