Skip to content

Editor API: POST /api/editor/bulk-review (batch reject/approve, single storage write per call) #701

Description

@teflonmusk

Why

Mass-rejecting cap-displaced signals at scale is currently bottlenecked by the per-call write pattern against the cap-state Durable Object. Empirically (this week's BM drain — 149 rejects across ~20 waves):

  • Safe parallel ceiling: 5–15 calls. Above that, the DO returns 503 storage event loop overloaded.
  • 503s are silently-successful sometimes. The canary (refire same ID, expect 400 already-rejected) revealed several waves where the 503 reply masked a successful write. This makes retry logic hard to write correctly.
  • Same root cause as the sBTC TooMuchChaining payout pattern. Both are per-call writes to a rate-limited backend; both bottleneck the EIC's daily close-out grind.

A single bulk endpoint that batches N actions into one write per call eliminates the throttle, kills the 503 ambiguity, and cuts a 30-signal cap-displacement sweep from ~20 waves of 5–15 to one call.

Proposed endpoint

POST /api/editor/bulk-review
Authorization: BIP-322 sig (same scheme as POST /api/editor/review)
Content-Type: application/json

Request body

{
  "actions": [
    { "signal_id": "uuid", "status": "rejected", "feedback": "REJECT — surplus to today's cap. Refile fresh tomorrow." },
    { "signal_id": "uuid", "status": "approved", "feedback": "" },
    { "signal_id": "uuid", "status": "rejected", "feedback": "REJECT — duplicate primary anchor of approved signal X." }
  ]
}
  • Max 50 actions per call. Above that, return 413 with max_actions: 50.
  • status: "approved" | "rejected" (mirrors single-review semantics).
  • feedback: required when status == rejected, optional when approved. Same length limit as single endpoint.
  • signal_id: must belong to a beat the calling editor has claimed; otherwise that action returns 403 (per-action, not whole-batch fail).

Response

{
  "results": [
    { "signal_id": "uuid", "success": true,  "status": "rejected" },
    { "signal_id": "uuid", "success": true,  "status": "approved", "included_in_brief": true, "rank": 7 },
    { "signal_id": "uuid", "success": false, "error": "already_rejected", "noop": true },
    { "signal_id": "uuid", "success": false, "error": "not_found" }
  ],
  "summary": { "total": 4, "succeeded": 2, "noop": 1, "failed": 1 }
}
  • One storage write per batch. The DO loads cap-state once, applies all actions in memory, writes once. This is the entire point.
  • Per-action errors do NOT fail the batch. A bad signal_id leaves the other 49 in place. The caller reads the per-action success field.
  • Idempotent on already_rejected / already_approved. Returns success: false, error: "already_*", noop: true instead of the current 400. Lets clients retry safely after a transport failure without distinguishing "my retry" from "a real conflict."

Backwards compat

The existing POST /api/editor/review single endpoint stays as-is. Bulk is purely additive; no migration needed for current tooling.

Why not just raise the DO's parallel-write ceiling

Considered. Two reasons batching beats it:

  1. Throughput ceiling on the DO is the wrong knob. Even if you raise it 4x, the next mass-event (a rubric change that retroactively flags 200 signals; a publisher-driven full-day re-grade) will hit the new ceiling. Batching scales O(1) in storage writes regardless of action count.
  2. Idempotency is a separate fix you'd want anyway. The noop: true semantic above lets retries be safe under network failure. Currently a 503 mid-write means the editor doesn't know if the action landed and the canary-refire-and-check-for-400 dance is the only way to find out.

Sequencing

Happy to draft the implementation if you want to scope it; the hard part is the DO's atomic batch-apply, the rest is request validation. Could pair on the DO change.

Related

  • The cap-displacement workflow this unblocks is the proposed v4.2 cap_displaced terminal status from Quality Rubric v4 — community consolidation (post-v3 proposals) #696 (Quality Rubric v4 consolidation). Bulk-review is the operational primitive that makes mass cap-close at end-of-day cheap.
  • The sBTC payout side has the same pattern (TooMuchChaining at ~25–30 chained txs); a POST /api/payout/batch would be the analogous fix on the payout side, but I'd punt that until this one ships and proves the pattern.

Tags: @whoabuddy

— DC, EIC trial Day 6

Metadata

Metadata

Assignees

No one assigned

    Labels

    apiAPI endpoints and contractseditor-systemEditor roles, review framework, rubricenhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions