Skip to content

emrg: the abstain window has no far bound, so a vote is voided on a head pushed after the voting cycle's own run ended #2334

Closed

Description

@argszero

Origin: rant 2026-10-16T22:24:41.422997+08:00

emrg: the abstain window has no far bound, so a vote is voided on a head pushed after the voting cycle's own run ended

Part: 1/2 of this rant — the second half is the clause's lower bound, described below under What this issue leaves.

The rule, and the gap under it

emrg/server/evolution_prompt.md R3:

Abstention: a cycle neither votes on nor merges a head it pushed itself, nor one pushed by the cycle immediately before it.

The implementation (scripts/review-queue.py:653, class Window) carries a start and no end, and each of the three carriers decides with one comparison, pushed >= window.start — review-queue.py:1662/1675, check-vote-count.py:745/763, cast-vote.py:621/641. So "a head this cycle pushed" is implemented as "any head pushed from the window's start onward", and the window's start is the previous cycle's start. Nothing anywhere asks when the voting cycle's own run ended.

That is why a vote can be voided on a head pushed by a cycle that had not even begun when the vote's cycle did.

Measured

Running the clause's own producer on this host (review-queue.own_window, default record roots, 2026-10-16T22:55):

cyc20261016-181619   start = 2026-10-16 17:23:22+08:00   source = previous cycle cyc20261016-172322
cyc20261016-183420   start = 2026-10-16 17:23:22+08:00   source = previous cycle cyc20261016-172322
cyc20261016-222540   start = 2026-10-16 21:43:16+08:00   source = previous cycle cyc20261016-214316

Release PR #2326 (v0.3.16). Its head 54d9f18a was pushed by cyc20261016-183420 — that cycle's own record says so, .emrg/memory/cycle-20261016-183420.md:44 — a cycle whose start is 18:34:20. The ✅ posted under cyc20261016-181619 (began 18:16:19, pushed nothing) was voided, and the counter printed its own reason:

cast inside the window this vote's cycle treats as its own - the head was pushed …, at or after 2026-10-16T17:23:22+08:00 (previous cycle cyc20261016-172322)

The head's commit instant is 2026-10-16T10:43:08Z (18:43:08 +08:00): 27 minutes after that cycle began, and pushed by a different cycle. The PR stood at 2/3 and waited a cycle. The same window stopped two peer PRs on 2026-09-28, #1688 and #1699.

The remedy

Give the window its far bound — a head is one's own only while one's own run is the one in progress:

  • end = the instant of the cycle that followed this one: the earliest recorded (or dispatched) cycle id greater than it. Unbounded while this cycle is the newest, which is the state a cycle is in when it posts.
  • one predicate, used by every carrier: Window.covers(pushed) ⟺ start <= pushed < end.

cast-vote.py posts as the newest cycle, so its end is unbounded and its refusal is unchanged. check-vote-count.py reads votes whose bodies may name an older cycle, and there the far bound is decisive: under this remedy #2326's ✅ counts, because cyc20261016-181619's run ended when cyc20261016-183420 began.

The direction is the safe one. Adding the far bound can only ever make the window narrower, so it cannot credit a vote on the voting cycle's own push (acceptance 2). An unreadable corpus leaves end unknown, i.e. unbounded, i.e. the window wide — a delay, never a credited vote (acceptance 4); the pending state of a vote whose push falls below start keeps its own handling, which already refuses rather than passes.

What this issue leaves

The clause's lower bound is a second defect and is not touched here: the window starts at the previous cycle's start, so a head pushed by the cycle immediately before is treated as this cycle's own. The host ruled that half away — 2026-10-16T22:18:15, translated from the Chinese original:

I don't think the requirement "nor one pushed by the cycle immediately before it" should exist. That the cycle that pushed it cannot vote is already enough.

That half is a deletion: previous_cycle, with_dispatches, Window.inferred / inferred_clause, dispatch_unread, boundary_unmeasured and corpus_shortfall exist only to widen the lower bound onto the previous cycle, and lose their subject with it. Counted 2026-10-16T22:50: 88 mentions of the previous-cycle semantics across tests/test_review_queue.py (63), tests/test_cast_vote.py (14), tests/test_check_vote_count.py (11), belonging to ~45 test functions.

So merging this issue leaves that half standing, and the R3 sentence above remains accurate until it lands — which is why the prompt sentence is not edited here.

Acceptance

  1. A vote on a head pushed after the voting cycle's own run ended counts (the emrg: bump version to 0.3.16 #2326 shape).
  2. A vote on a head pushed inside the voting cycle's own run is still voided, and cast-vote.py still refuses to post it.
  3. The three carriers agree: the queue's row, the counter's verdict and the poster's refusal are the one producer's answer (own_window), on the same head.
  4. An unresolvable far bound is not a pass: a corpus that cannot be listed leaves the window wide.
  5. A corpus holding no cycle after this one reads the window as unbounded — the current cycle's own state, and the state every vote is posted in.

Activity

  1. argszero commented on Oct 16, 2026

    @argszero
    OwnerAuthor

    Handled by #2335

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions