Skip to content

checkout_next reports no eligible work while eligible work exists — assignee predicate applied after LIMIT #15965

Closed

Description

@mrveiss

Problem

checkout_next can report "no eligible work item" while eligible work exists. On base now, merged in #15920 (5ab30ac80b) past an unresolved Major review thread.

autobot-backend/llc/services/work_item_queue.py:95-106:

eligible = (
    select(LLCWorkItem)
    .where(
        LLCWorkItem.company_id == uuid.UUID(company_id),
        LLCWorkItem.status == WorkItemStatus.READY.value,
        LLCWorkItem.checkout_run_id.is_(None),
    )
    .order_by(*backlog_order())
    .limit(CHECKOUT_CANDIDATES)          # = 10
)
for item in (await session.execute(eligible)).scalars().all():
    if item.assignee_agent_id is not None and str(item.assignee_agent_id) != agent_id:
        continue

The assignee predicate is applied in Python, after the SQL LIMIT. The function's own docstring states the rule the query is missing:

Eligible means: this company's, ready, unclaimed, and either unassigned or assigned to this agent.

That final clause is not in the WHERE. So the query fetches the first 10 ready, unclaimed items regardless of assignee, and the loop then discards the ones belonging to other agents.

Failure scenario

A company has 12 ready, unclaimed work items. The first 10 in backlog_order() are assigned to other agents; items 11 and 12 are unassigned.

Agent A calls checkout_next. The query returns rows 1-10, the loop discards all ten on the assignee test, the function returns None, and the route answers "No eligible work item" — while two items it is entitled to sit just past the limit.

The failure scales with contention: the more assigned work a company has queued ahead, the more reliably every agent is told there is nothing to do. At CHECKOUT_CANDIDATES = 10 it needs only ten assigned items to starve the queue completely.

Why it went unnoticed

None is a legitimate, expected return. The docstring says so explicitly:

None when nothing is eligible: an ordinary answer, not an error. An agent asking for work when there is none is the common case.

So the bug's output is indistinguishable from correct operation. There is no error, no log line, no metric — an agent that should be working simply idles, and the system reports the common case. Nothing in the response says whether the queue was empty or merely mis-queried.

Fix

Move the predicate into the query, so LIMIT applies to rows that are actually eligible:

.where(
    LLCWorkItem.company_id == uuid.UUID(company_id),
    LLCWorkItem.status == WorkItemStatus.READY.value,
    LLCWorkItem.checkout_run_id.is_(None),
    or_(
        LLCWorkItem.assignee_agent_id.is_(None),
        LLCWorkItem.assignee_agent_id == agent_id,
    ),
)

The Python continue then becomes dead and should go with it — leaving it would preserve a second, now-unreachable copy of the rule.

The CheckoutConflict continue below is unrelated and must stay: that one handles a race between SELECT and fence, not eligibility.

Acceptance criteria

  • The assignee predicate is in the SQL WHERE, evaluated before LIMIT
  • The post-limit Python assignee filter is removed, not left as a redundant second copy
  • The CheckoutConflict retry loop is unchanged
  • Regression test: 10+ ready unclaimed items assigned to another agent, ordered ahead of one unassigned item; checkout_next returns the unassigned item rather than None
  • A test pins the boundary at CHECKOUT_CANDIDATES exactly, so raising the constant cannot silently mask the bug instead of fixing it
  • Docstring and query agree — whichever is authoritative, they state the same rule

Provenance

Found by review on #15920 and marked Major; the thread was never opened and the PR merged with it unresolved. Four sessions reviewed that PR and each cleared its own scope, so the gap sat exactly where every reviewer assumed another had been. Recorded because the review process failed here in the same way the code does: an aggregate of clearances has no field for what nobody examined.

Activity

  1. mrveiss commented on Sep 7, 2026

    @mrveiss
    OwnerAuthor

    Scope confirmation from b9's backend sweep: this is a single site. No other service applies an eligibility predicate after a LIMIT, so the fix does not need widening and a reviewer should not go looking for siblings.

    Companion issue from the same PR: the /heartbeat/report telemetry fields, filed separately. Both are the same failure shape — a response indistinguishable from the correct one.

  2. mrveiss commented on Sep 7, 2026

    @mrveiss
    OwnerAuthor

    Do not start this — it is already fixed in flight by #15964. Verified against that PR's head (issue-15905-followup):

    work_item_queue.py:107    .where(
                       :116        or_(
                       :117            LLCWorkItem.assignee_agent_id.is_(None),
                       :118            LLCWorkItem.assignee_agent_id == uuid.UUID(agent_id),
                       :122    .limit(CHECKOUT_CANDIDATES)
    

    The predicate is inside the WHERE and above the LIMIT, which is exactly what this issue asks for. It is 46's follow-up to the three threads #15920 merged past.

    Close this with that as evidence once #15964 lands — not before. Two acceptance criteria here are not obviously satisfied by the diff above and should be checked against the merged code rather than assumed:

    • the post-limit Python assignee filter is removed, not left as a redundant second copy — a second copy would be dead today and wrong the next time either is edited
    • a test pins the boundary at CHECKOUT_CANDIDATES exactly, so raising the constant cannot substitute for the fix. This is the criterion worth defending: a larger limit makes the symptom rarer and reads as a repair.

    Leaving the issue open rather than closing it now, because a merge is not a closure argument here and the criteria above are what closure has to cite.

  3. mrveiss commented on Sep 7, 2026

    @mrveiss
    OwnerAuthor

    Verified per criterion against merged origin/Dev_new_gui (#15920 + #15964). Four of six met — leaving this open.

    # criterion verdict evidence on merged base
    1 assignee predicate in the SQL WHERE, before LIMIT met work_item_queue.py — .where(..., claimable_by(uuid.UUID(agent_id))) precedes .limit(CHECKOUT_CANDIDATES).
    2 post-limit Python filter removed, not left as a second copy met The loop over the result does nothing but service.checkout(...); no assignee_* comparison survives outside claimable_by.
    3 CheckoutConflict retry loop unchanged met except CheckoutConflict: … continue, same shape as before.
    4 regression test: 10+ ready items assigned to another agent ordered ahead of one unassigned item, checkout_next returns the unassigned item NOT met Only two files on base mention checkout_next: test_agent_stub_routes_15905.py (route behaviour, backlog ordering, cross-company) and test_checkout_excludes_user_assigned_15964.py (the predicate's shape). Neither seeds a population larger than the limit.
    5 a test pins the boundary at CHECKOUT_CANDIDATES exactly NOT met Nothing on base references the constant except its definition. Your read was right, and this is the criterion that matters most: without it, raising the constant from 10 to 100 masks the bug instead of fixing it, and every existing test still passes.
    6 docstring and query state the same rule met Docstring: "either unassigned or assigned to this agent"; claimable_by requires both assignee columns null for "unassigned", which is what "unassigned" means. They agree, and claimable_by's own docstring states the two-column rule explicitly.

    What is left is one test file. Criteria 4 and 5 are the same fixture: seed CHECKOUT_CANDIDATES items assigned to another agent, ordered ahead of one unassigned item, assert checkout_next returns the unassigned one — then assert the boundary by reading the constant rather than hard-coding 10, so the test moves with it.

    Worth noting why the tests that exist cannot stand in: they were written against the predicate's shape (mine, in #15964) and against route behaviour. Neither constructs a population that exceeds the limit, which is the only condition under which the original bug appeared. A test of the fix's shape is not a test of the failure it fixed.

  4. mrveiss commented on Sep 7, 2026

    @mrveiss
    OwnerAuthor

    Criteria 4 and 5 are addressed in PR #15988.

    Criterion 5 is demonstrated rather than asserted. With the original defect restored and the constant raised 10 → 100, I built a hard-coded-10 variant of the same file and ran both under that mutation:

    under (defect + constant raised to 100) result
    hard-coded-10 variant 1 passed — masked
    the committed file, reading the constant 1 failed — still catches it

    That is the property the criterion asked for, measured: raising the constant cannot be substituted for fixing the predicate.

  5. mrveiss commented on Sep 7, 2026

    @mrveiss
    OwnerAuthor

    #15988 merged. All six criteria verified against merged origin/Dev_new_gui. Closing.

    # criterion verdict evidence on merged base
    1 assignee predicate in the SQL WHERE, before LIMIT met work_item_queue.py — .where(..., claimable_by(uuid.UUID(agent_id))) precedes .limit(CHECKOUT_CANDIDATES).
    2 post-limit Python filter removed met The loop over the result does nothing but service.checkout(...); no assignee_* comparison survives outside claimable_by.
    3 CheckoutConflict retry loop unchanged met except CheckoutConflict: … continue, same shape.
    4 regression test: 10+ items assigned to another agent ordered ahead of one unassigned item met test_checkout_next_limit_boundary_15965.py::test_an_eligible_item_below_a_full_window_is_still_returned seeds exactly CHECKOUT_CANDIDATES decoys ahead of one unassigned item and asserts the unassigned one is returned.
    5 a test pins the boundary at CHECKOUT_CANDIDATES exactly met 13 references to the constant in that file and zero occurrences of range(10) or position=10. The window is full whatever the constant becomes.
    6 docstring and query state the same rule met Docstring says "either unassigned or assigned to this agent"; claimable_by requires both assignee columns null for "unassigned", and states the two-column rule explicitly in its own docstring.

    Criterion 5 was demonstrated, not asserted. With the original defect restored and the constant raised 10 → 100, I built a hard-coded-10 variant of the same file, ran both under that mutation, and deleted it:

    under (defect + constant raised to 100) result
    hard-coded-10 variant 1 passed — masked
    the committed file, reading the constant 1 failed — still catches it

    That is the criterion's own wording turned into a measurement: a test pinned to the boundary rather than to a number that happens to equal it today. It is also the answer to how do you know raising the constant would have hidden it — because I raised it and watched the other test pass.

    Why the tests that already existed could not stand in. test_checkout_excludes_user_assigned_15964.py asserts the predicate's shape — I wrote it, and it is a test of the fix. test_agent_stub_routes_15905.py covers route behaviour and ordering. Neither builds a population exceeding CHECKOUT_CANDIDATES, which is the only condition under which the defect appears. A test of the fix's shape is not a test of the failure it fixed.

    Two contrasts accompany it, because "return something below the window" is otherwise satisfied by a worse bug than the one being fixed: a window of only another agent's items must still yield None, and an item already assigned to this agent below a full window must be claimable.

    Suite at merge: llc/tests/ 1828 passed / 9 failed against 1824 / 9 on base — the same nine, plus this file's four.

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

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions