CB-590 follow-up: per-source reminder budgets for the push loop #90

Merged
ltms merged 2 commits from worker/cb590fix-185e9a-10 into main 2026-08-16 17:28:58 +02:00
Member

Fixes the regression a reviewer found in PR #84: collapsing the CB-307 (reply) and CB-588 (ticket) nudge schedules into one per lead also collapsed them onto one shared reminderCount. A reply stream that exhausted its own budget could then make decide() STOP even when a ticket for the same lead had coalesced onto the still-active schedule and had never been nudged - stranding it, since stopOrRestart cannot rescue work already present in that tick's "before" snapshot.

Fix: decide(lead, replyReminderCount, ticketReminderCount) tracks each source's budget independently. INJECT fires while either source is still under its own cap; STOP only when both are exhausted. Still exactly one schedule per lead - stopOrRestart's snapshot-diff race guard is untouched.

Also adds the reply-arm mirror of the existing stopOrRestart race/stale tests (only the ticket arm was pinned before), and a direct regression test for the cross-source case (oneExhaustedSourceDoesNotBlockANudgeForTheOtherSource) that fails against the pre-fix shared-counter behavior - verified locally by temporarily reverting decide() to a shared-counter simulation: it failed with expected INJECT but was STOP.

mvn clean install: Tests run: 814, Failures: 0, Errors: 0 - BUILD SUCCESS. ReplyPushLoopTest: Tests run: 41, Failures: 0.

Fixes the regression a reviewer found in PR #84: collapsing the CB-307 (reply) and CB-588 (ticket) nudge schedules into one per lead also collapsed them onto one shared reminderCount. A reply stream that exhausted its own budget could then make decide() STOP even when a ticket for the same lead had coalesced onto the still-active schedule and had never been nudged - stranding it, since stopOrRestart cannot rescue work already present in that tick's "before" snapshot. Fix: decide(lead, replyReminderCount, ticketReminderCount) tracks each source's budget independently. INJECT fires while either source is still under its own cap; STOP only when both are exhausted. Still exactly one schedule per lead - stopOrRestart's snapshot-diff race guard is untouched. Also adds the reply-arm mirror of the existing stopOrRestart race/stale tests (only the ticket arm was pinned before), and a direct regression test for the cross-source case (oneExhaustedSourceDoesNotBlockANudgeForTheOtherSource) that fails against the pre-fix shared-counter behavior - verified locally by temporarily reverting decide() to a shared-counter simulation: it failed with expected INJECT but was STOP. mvn clean install: Tests run: 814, Failures: 0, Errors: 0 - BUILD SUCCESS. ReplyPushLoopTest: Tests run: 41, Failures: 0.
agent added 2 commits 2026-08-16 17:26:39 +02:00
CB-590: collapse the CB-307 and CB-588 nudge schedules into one per lead
CI / build (pull_request) Successful in 1m0s
CI / contract (pull_request) Successful in 1m33s
78ca24dc3f
Both reply-queued and ticket-terminal nudges could independently decide
to inject into the same lead pane in the same window, since they ran as
two separate schedules keyed differently (worker target vs. lead) that
never checked each other. Replace both with a single per-lead schedule
(activeLeads) that drains pending reply targets and pending tickets
together, sends at most one combined nudge per tick, and shares one
reminder cap across both sources — so two injections into the same pane
can no longer overlap, and work queued while the lead is busy is never
lost, only deferred.
CB-590 follow-up: give each nudge source its own reminder budget
CI / contract (pull_request) Successful in 43s
CI / build (pull_request) Successful in 1m46s
88b9503c3b
decide(lead, reminderCount) shared one counter across the reply and
ticket sources after PR #84 collapsed both onto a single per-lead
schedule. A reply stream that used up the whole budget could then
make decide() STOP even for a ticket that had never been nudged and
had coalesced onto the same still-active schedule — stranding it with
no live schedule left, since stopOrRestart's racedIn check does not
save work that was already present in the "before" snapshot.

decide() now tracks a per-source count (replyReminderCount,
ticketReminderCount) and returns INJECT while either source is still
under its own cap, STOP only when both are exhausted. Still exactly
one schedule per lead; stopOrRestart's snapshot-diff logic is
untouched.
ltms merged commit 8d4206c2b5 into main 2026-08-16 17:28:58 +02:00
Sign in to join this conversation.