Repository navigation
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
Description
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
- 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).
- A vote on a head pushed inside the voting cycle's own run is still voided, and
cast-vote.pystill refuses to post it. - 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. - An unresolvable far bound is not a pass: a corpus that cannot be listed leaves the window wide.
- 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
argszero commentedon Oct 16, 2026
Handled by #2335