Skip to content

🐛 fix(moderation): recover nodes stranded in pending#92

Merged
mathix420 merged 3 commits into
stagingfrom
fix/moderation-reconcile-stuck-pending
Jul 1, 2026
Merged

🐛 fix(moderation): recover nodes stranded in pending#92
mathix420 merged 3 commits into
stagingfrom
fix/moderation-reconcile-stuck-pending

Conversation

@mathix420

Copy link
Copy Markdown
Owner

Why

Nodes were ending up stuck in pending with no auto-moderation, forcing a manual admin Re-moderate. Confirmed in production: issue #68 (edited at 09:29, sat pending ~7h until a manual remod at 16:27) and case studies #65 / #78 (never auto-approved at creation). Two independent root causes.

What

1. Safety net for failed/never-fired triggers — review:reconcile-pending

triggerModeration fires the moderation Workflow best-effort and swallows every failure (missing binding, transient create() error). Cloudflare Workflows give durability once a run exists — but if the run is never created or dies before recording a verdict, the node is stranded pending with nothing to recover it. There was no reconciliation job.

New hourly scheduled task (0 * * * *) that finds nodes pending for > 30 min with no terminal moderation verdict since their last edit and re-submits them to the durable Workflow.

  • updated_at moves only on a real edit (the Workflow's own approve/curate/relocate writes don't touch it), so a node edited after a prior verdict correctly re-qualifies as stuck if its re-trigger failed.
  • The decision-action guard (flag_uncertain / request_info / approve / reject / …) keeps nodes that are deliberately pending out of the candidate set; the 30-min grace keeps in-flight reviews out.
  • Batch-capped at 100/run; fires sequentially to stay under the instance-creation rate limit.

2. Case-study approval no longer coupled to enrichment

curate + finalize ran together under one Promise.all, so a curate/location failure threw out of the run before the approval committed, reverting the case study to pending (what stranded #65). Split to mirror the issue pipeline:

  • finalizeCaseStudy commits the approval first (status + embedding from original text + approve audit), taking prep.
  • A new best-effort enrichCaseStudy pass runs after (curate ∥ location, each .catch-guarded). Curation now lands as its own curate audit log, exactly like issues.

Verification

Notes

  • triggerModeration intentionally stays best-effort on the hot write path (a moderation hiccup never fails a user's create/edit); the hourly reconciler is the backstop.
  • A deeper Workflows-native upgrade (persist instance.id per node, drive the reconciler off MODERATION_WORKFLOW.get(id).status() / restart()) is possible but needs a schema migration and is deferred — the heuristic reconciler recovers every case observed here.

🤖 Generated with Claude Code

Nodes could end up stuck `pending` with no auto-moderation, forcing a
manual admin "Re-moderate" (seen in prod for issue #68 and case studies
#65 / #78). Two independent causes, two fixes.

1. Trigger boundary has no safety net. `triggerModeration` fires the
   moderation Workflow best-effort and swallows failures; if the run is
   never created (transient error / missing binding) or dies before a
   verdict, the node sits `pending` forever with nothing to recover it.
   Add `review:reconcile-pending`, an hourly scheduled task that finds
   nodes pending > 30 min with no terminal verdict since their last edit
   and re-submits them to the durable Workflow. The decision-action +
   grace-window guards skip nodes that are deliberately pending (flagged
   uncertain / awaiting author info) or still in flight.

2. Case-study approval was coupled to enrichment. `curate` + `finalize`
   ran together under one Promise.all, so a curate/location failure threw
   out of the run *before* the approval committed, reverting the case
   study to `pending` (what stranded #65). Split it like the issue
   pipeline: `finalizeCaseStudy` commits the approval first, then a
   best-effort `enrichCaseStudy` pass (curate ∥ location, each guarded)
   runs after. Curation now lands as its own `curate` audit log.

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

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown

🧹 Code quality

Check Summary
vize lint 0 errors · 326 warnings
vize format all files formatted
fallow audit pass · 0 dead-code · 14 complexity · 3 dupes (8 files)

updated for d5e1182 · run #252

mathix420 and others added 2 commits July 1, 2026 10:56
The two functions introduced by the previous commit tripped `fallow
audit`'s new-only CRAP gate. Refactor both under threshold (no
suppressions):

- applyCaseStudyCurate: extract the field-cleaning into `compact` /
  `cleanRows` null-strippers and the embed text into `curatedCaseStudyText`,
  leaving the function a flat patch-builder. `Metric`/`Source`/`LinkRow`
  are a required key plus optionals, so stripping nulls is equivalent to
  the old explicit per-field reconstruction.
- review:reconcile-pending: fold the two stuck-node queries into one
  UNION helper (`findStuckPending`) so the task callback is a short
  fetch → fire → log.

verdict: pass · 0 introduced complexity/duplication/dead-code findings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`vize fmt` (the CI format:check gate) reflowed the extracted `compact`
helper signature. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mathix420
mathix420 merged commit 42733e4 into staging Jul 1, 2026
6 checks passed
@mathix420
mathix420 deleted the fix/moderation-reconcile-stuck-pending branch July 1, 2026 09:09
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