Skip to content

bug(scheduler): TaskComplexity aliases collapse — every non-simple workflow silently gets the security-scan priority multiplier #13806

Closed

Description

@mrveiss

Discovered while fixing #13699/#13730 (the orchestrator planning path).

Finding

TaskComplexity declares 5 members but only 2 distinct values:

# autobot-backend/autobot_types.py
class TaskComplexity(Enum):
    SIMPLE = "simple"
    COMPLEX = "complex"
    RESEARCH = "complex"        # <- alias of COMPLEX
    INSTALL = "complex"         # <- alias of COMPLEX
    SECURITY_SCAN = "complex"   # <- alias of COMPLEX

In Python, Enum members sharing a value become aliases of the same object, not distinct
members. TaskComplexity.RESEARCH is TaskComplexity.COMPLEX is True. The comment calls them
"legacy values for backward compatibility (map to COMPLEX)", which is accurate — but several
call sites are written as though they are distinguishable, and they are not.

This is a live scheduler bug, not just cosmetics

autobot-backend/workflow_scheduler.py:340-346 builds a per-complexity priority multiplier
(Issue #376, "use named constants"):

complexity_multiplier = {
    TaskComplexity.SIMPLE:        WorkflowConfig.COMPLEXITY_SIMPLE,          # 0.8
    TaskComplexity.RESEARCH:      WorkflowConfig.COMPLEXITY_RESEARCH,        # 1.0
    TaskComplexity.INSTALL:       WorkflowConfig.COMPLEXITY_INSTALL,         # 1.1
    TaskComplexity.COMPLEX:       WorkflowConfig.COMPLEXITY_COMPLEX,         # 1.2
    TaskComplexity.SECURITY_SCAN: WorkflowConfig.COMPLEXITY_SECURITY_SCAN,   # 1.3
}

Four of those five keys are the same object, so the literal collapses to two entries and
last write wins:

$ # reproduction with the same member definitions
dict written with 3 distinct keys -> 1 entry
TaskComplexity.RESEARCH is COMPLEX: True
lookup RESEARCH gives: <the last-written value>

Because SECURITY_SCAN is written last, every non-simple workflow is scheduled with the
security-scan multiplier of 1.3
:

Complexity Intended multiplier Actual
SIMPLE 0.8 0.8 ✅
RESEARCH 1.0 1.3
INSTALL 1.1 1.3
COMPLEX 1.2 1.3
SECURITY_SCAN 1.3 1.3 ✅

The per-complexity tuning #376 added has therefore never taken effect. Reordering the dict
literal would silently change scheduling priority across the board — which is the hazard here:
the behaviour depends on source order, invisibly.

Second consequence: template metadata is lossy

workflow_templates/{research,sysadmin,security,analysis}.py set complexity=TaskComplexity.RESEARCH
/ .INSTALL / .SECURITY_SCAN on ~10 templates. All of them persist and compare as COMPLEX,
so no consumer can distinguish a research template from an install template by complexity.

Third: it makes tests look broader than they are

orchestration/plan_steps_e2e_test.py iterated [SIMPLE, RESEARCH, INSTALL, COMPLEX] as four
"complexity levels" — really SIMPLE plus COMPLEX three times. Corrected in #13730, with a
comment, but the underlying trap remains for the next author.

Options

  1. Collapse honestly — delete the three aliases, migrate the ~13 call sites to
    COMPLEX, and drop the now-unreachable COMPLEXITY_RESEARCH/INSTALL/SECURITY_SCAN
    constants. Behaviour is then what it already is, just visible. Scheduling changes only if
    the collapsed multiplier is chosen deliberately.
  2. Make them real — give each a distinct value so the Code Smell: Replace 537 magic numbers with named constants #376 tuning works as designed. This
    changes scheduling behaviour and needs the persisted-value migration considered
    (templates store complexity).
  3. Keep the aliases but fix the scheduler to key on something non-collapsing, and add a guard
    test. Least churn, but leaves the trap for the next reader.

Recommendation: 1, unless the per-complexity tuning is actually wanted, in which case 2
with a migration. Either way the dict must stop depending on source order.

Acceptance criteria

  • Decide between collapsing the aliases and making them distinct; record the reasoning.
  • workflow_scheduler.py no longer builds a dict whose keys silently merge.
  • A test asserts the multiplier actually applied per complexity, so a source-order change
    cannot alter scheduling unnoticed.
  • If aliases are retained, a guard test asserts no dict/match is keyed on more than one
    alias of the same member.

Activity

  1. mrveiss commented on Aug 10, 2026

    @mrveiss
    OwnerAuthor

    Guard landed ahead of the decision — PR #13881

    The collapse-vs-distinct choice is still yours; it changes scheduling priority for every non-simple workflow, so I have not pre-empted it. What #13881 does remove is the silence around it.

    Reproduced against the current tree:

    canonical members  : [SIMPLE, COMPLEX]
    RESEARCH is COMPLEX: True
    dict written with 5 keys -> 2 entries: {'SIMPLE': 0.8, 'COMPLEX': 1.3}
    

    Tests only, no behaviour change:

    • the applied multipliers are pinned (0.8, and COMPLEXITY_SECURITY_SCAN for everything else), so reordering the literal fails a test instead of retuning the scheduler
    • a repo-wide guard fails on any new dict whose TaskComplexity keys silently merge — verified by planting one
    • workflow_scheduler.py is exempted by name; removing that exemption is part of resolving this issue

    One thing worth knowing before you pick

    "Collapse honestly" is not behaviour-neutral in the obvious way. Writing the natural form:

    {TaskComplexity.SIMPLE: COMPLEXITY_SIMPLE, TaskComplexity.COMPLEX: COMPLEXITY_COMPLEX}

    moves every non-simple workflow from 1.3 → 1.2. Preserving today's behaviour requires writing COMPLEXITY_SECURITY_SCAN as the COMPLEX multiplier — correct, but it reads like a mistake, so it needs a comment saying it is deliberate.

    So the real fork is:

    1. Collapse, keep 1.3 — zero behaviour change, honest source, odd-looking constant name. Cheapest.
    2. Collapse, adopt 1.2 — the source reads naturally and non-simple workflows get slightly lower priority. A real (small) scheduling change.
    3. Make them distinct — the Code Smell: Replace 537 magic numbers with named constants #376 tuning finally works as designed; needs the persisted-template migration considered, since ~10 templates store complexity.

    My recommendation is 2 if you want the constant names to mean what they say and are comfortable with a one-notch priority drop, otherwise 1. 3 only if per-complexity tuning is genuinely wanted — it is the only option that changes relative ordering between workflow kinds.

    Labelling needs-decision and moving on; the guard makes whichever you pick safe to execute.

  2. added
    needs-decisionBlocked on an owner decision; options and a recommendation are on the issue
    on Aug 10, 2026
  3. mrveiss commented on Aug 10, 2026

    @mrveiss
    OwnerAuthor

    Guard landed — PR #13881 merged

    Verified in the base branch:

    $ git show origin/Dev_new_gui:autobot-backend/tests/orchestration/test_task_complexity_aliases.py
    (present)
    

    Three tests now hold the line:

    • the applied multipliers are pinned — 0.8 for SIMPLE, COMPLEXITY_SECURITY_SCAN for everything else. That assertion looks wrong deliberately: it records what runs, not what the source reads as. Reordering the dict literal now fails a test instead of silently retuning the scheduler.
    • a repo-wide guard fails on any new dict whose TaskComplexity keys merge (verified by planting one)
    • the alias premise itself is asserted, so giving the aliases distinct values fails here first and points at this issue rather than failing the multipliers mysteriously

    workflow_scheduler.py stays exempt by name — removing that exemption is part of resolving this issue.

    Still needs your decision

    Nothing here changed behaviour. The three options and the recommendation are in the earlier comment; the short version is that "collapse honestly" is not behaviour-neutral — writing the natural {SIMPLE: COMPLEXITY_SIMPLE, COMPLEX: COMPLEXITY_COMPLEX} moves every non-simple workflow from 1.3 to 1.2. Whichever you pick, the guard makes it safe to execute and impossible to get wrong silently.

    Staying open with needs-decision.

  4. mrveiss commented on Sep 6, 2026

    @mrveiss
    OwnerAuthor

    Removing needs-decision: the tree answers which option applies. autobot-backend/autobot_types.py:19-21 still has RESEARCH = INSTALL = SECURITY_SCAN = "complex", and tests/orchestration/test_task_complexity_aliases.py pins that collapse (assert TaskComplexity.RESEARCH is TaskComplexity.COMPLEX) rather than fixing it. Since nothing distinguishes the three today, Option 1 — collapse honestly — is what the code already does; the work is making the declaration say so.

  5. removed
    needs-decisionBlocked on an owner decision; options and a recommendation are on the issue
    on Sep 6, 2026
  6. modified the milestones: Backlog, v0.15.0 on Sep 12, 2026
  7. mrveiss commented on Oct 5, 2026

    @mrveiss
    OwnerAuthor

    Closed by PR #17965, merged as 176ce734d8. Verified against merged main.

    Criterion Evidence on main
    The aliases are gone TaskComplexity has 2 members; the five-key literal named three Enum aliases of COMPLEX
    One constant per member ssot_constants.py carries exactly COMPLEXITY_SIMPLE = 0.8 and COMPLEXITY_COMPLEX = 1.3, replacing five
    No behaviour change 1.3, not the 1.2 the source appeared to declare

    The number is the finding

    COMPLEXITY_SECURITY_SCAN was written last into the old five-key literal and overwrote COMPLEXITY_COMPLEX, because three of the five keys were Enum aliases resolving to the same member. So every non-simple workflow has always been scheduled at 1.3 while the source read as 1.2.

    Collapsing the aliases was held to zero behaviour change, which means keeping 1.3. Re-tuning it is a scheduling decision and is deliberately not part of this issue.

    A test that proved nothing, found in review

    The multiplier test built a mapping from the same two constants it then asserted on, and never called _calculate_priority_score. It passed whatever the scheduler actually applied — including 1.0, or the two factors swapped.

    It now compares the ratio of two real scores with every other input held constant. A future-dated scheduled_time keeps the overdue bonus out, since that is time-dependent and is added before the complexity factor multiplies.

    Mutation-verified rather than asserted — with complexity_factor = 1.0:

    AssertionError: the scheduler applies a complexity ratio of 1.0, not the 1.625
    these constants declare -- the mapping and the code that uses it have diverged
    

    That is the same shape as the defect this issue is about: a declaration and the code that consumes it drifting apart, with nothing comparing them. The guard now compares them.

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

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions