Skip to content

Deferred fix: future-valued publishedAt remove = reschedule (drained-map re-arm) - #338

Draft
JustMaier wants to merge 1 commit into
mainfrom
fix/deferred-remove-reschedule
Draft

Deferred fix: future-valued publishedAt remove = reschedule (drained-map re-arm)#338
JustMaier wants to merge 1 commit into
mainfrom
fix/deferred-remove-reschedule

Conversation

@JustMaier

Copy link
Copy Markdown
Contributor

The bug

The #335 trigger guard emits {op: remove, field: publishedAt, value: <future ts>} for scheduled posts. The ops processor ignored the carried value: on a deferred slot ANY remove of the schedule field = unschedule (slot leaves the deferred map, activates as draft — Tf activation destroyed); on a not-alive non-deferred slot the ops were memoized confirmed-deleted and dropped forever. July heal-era removes drained bitdex-1's deferred map through this path: ~93K scheduled images with NO activation path at their publish dates (earliest Aug 31). Found by the #3717 adversarial review; state confirmed empirically on bitdex-1.

The fix

get_future_schedule() reads a future timestamp from a Set OR value-bearing Remove on the deferred source field. Deferred slot → reschedule at that time (doc stores the schedule as a SET so activation replays a correct publishedAt). Not-alive non-deferred slot → re-ARM (before the confirmed-deleted memo — the future timestamp is what discriminates a drained scheduled slot from a deleted one). Null/past-valued removes keep unschedule semantics (genuine unpublish). PG-deleted rows can't reach the arming path (emission queries join live Image rows).

Why this also heals

Once deployed, one pass of the (currently gated) scheduled sweep in civitai #3717 re-arms every drained slot on both pods — the sweep becomes the standing repair mechanism instead of the thing that causes the damage.

Tests

3 new: reschedule-via-future-remove, past-valued remove still unschedules, drained-slot re-arm. Full --lib (server,pg-sync): 1317 passed, 2 pre-existing flakes (query_stream Win-timing; bound_store passes in isolation).

🤖 Generated with Claude Code

…an unschedule

The #335 trigger guard emits {op: remove, field: publishedAt, value: <ts>}
for scheduled posts — the remove hides the entity NOW, and the value says
WHEN it publishes. The ops processor ignored the value: any remove of the
deferred source field on a deferred slot took the unschedule branch (slot
dropped from the deferred map, activated as a draft), and on a not-alive
non-deferred slot the ops were memoized as confirmed-deleted and dropped
forever. July heal-era removes drained bitdex-1's deferred map through
exactly this path, leaving ~93K scheduled images with no activation path
at their publish time (2026-08-07 incident).

- get_future_schedule(): reads a FUTURE timestamp from a Set OR a
  value-bearing Remove on the deferred source field (LIFO last-wins,
  ms_to_secs honored; null/past resets — those remain unschedules).
- Deferred branch: future-valued remove reschedules at the carried time;
  the doc stores the schedule as a SET so Tf activation replays a correct
  publishedAt with no re-emit dependency.
- Not-alive discrimination chain: a future schedule in the ops re-ARMS the
  slot (deferred_alive at the carried time + doc write), checked BEFORE the
  confirmed-deleted memo — a drained scheduled slot is indistinguishable
  from a deleted one by doc-presence alone; the future timestamp is the
  discriminator. PG-deleted rows cannot reach this path (fan-out/re-emit
  queries join live Image rows).

Unblocks the scheduled sweep (civitai #3717, gated off) which then becomes
the standing re-arm heal for the drained maps. Deadline: before 2026-08-31
(earliest affected publish dates).

Tests: deferred reschedule-via-remove, past-valued remove still
unschedules, drained-slot re-arm (doc-present, below HWM, not deferred).
Full --lib suite: 1317 passed; 2 known flakes (query_stream Win-timing,
bound_store passes in isolation).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@JustMaier
JustMaier marked this pull request as draft August 7, 2026 01:38
@JustMaier

Copy link
Copy Markdown
Contributor Author

HOLD — do not merge. The adversarial sweep (2026-08-07) confirmed two criticals in this fix as written:

  1. Dedup order-dependence: get_future_schedule's last-op-wins assumes op order, but dedup_entity_ops routes same-field value-bearing removes (reschedule = remove(OLD)+remove(NEW) in one row, no Set) through multi_value_net — an AHashMap whose iteration order randomizes which remove is 'last'. A reschedule resolves by coin flip; wrong pick = content publicly visible days early, or a reverted draft auto-publishing. Also deterministic mis-order when a Set is present (removes re-emitted before sets).
  2. Fan-out deferred-reach path unpatched: apply_fanout_to_deferred_slots (queryOpSet path) still treats the guarded remove as an unschedule — this fix only covered the direct-op entrance.

Also confirmed major: future-only arming leaves past-Tf drained slots memoized confirmed-deleted (unhealable through the ops pipeline).

Redesign direction: resolve ambiguity at EMISSION (fan-out should not emit the old-side publishedAt remove — one unambiguous schedule op per row), plus patch both engine entrances, plus a past-Tf arming story. Full findings: docs/_in/adversarial-sweep-findings-2026-08-07.md (landing on main separately).

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