Skip to content

a skipped manager leaves imported subordinates with a dangling reports_to #14811

Closed

Description

@mrveiss

A skipped manager leaves its subordinates pointing at a UUID that was never inserted

autobot-backend/llc/services/portability.py

The import pre-mints an id for every agent in the template before inserting any
of them (two-pass, to avoid topology-ordering problems):

agent_id_map = {a.get("agent_id",""): str(uuid.uuid4()) for a in agents_list if a.get("agent_id")}

reports_to is then remapped through that map. But an agent can be skipped —
already exists, and now also an unknown or unrunnable adapter_type — while its
pre-minted id stays in the map. Any subordinate that does import gets a
reports_to pointing at a row that was never created.

agent_org_nodes.reports_to is a plain indexed String with no foreign key
(migrations/versions/20260315_009_agent_org_hierarchy.py), so nothing rejects it.

Pre-existing, but wider now

The "already exists — skipped" path has had this shape all along. #14808 widens it
materially: a CLI-less host can now skip every agent declaring a given adapter,
so an entire manager subtree can vanish while its reports import and hang off
nothing.

Fix

Track which agents were actually inserted rather than which were pre-minted, and
either null a subordinate's reports_to when its manager was skipped, or add an
explicit warning naming the orphaned relationship. Nulling is probably right — an
agent reporting to nobody is a state the org chart already handles, whereas one
reporting to a phantom is not.

Acceptance criteria

  • A template where a manager is skipped and a subordinate is not leaves the
    subordinate with a resolvable reports_to, or none
  • The import warns when a relationship is dropped, so it is visible
  • A test covers manager-skipped-subordinate-kept and fails if the remap goes
    back to using pre-minted ids unconditionally
  • Both skip reasons are covered — already-exists and adapter-gated — since they
    reach the same code by different routes

Found reviewing #14808. Not introduced there; that PR is what makes it reachable
often enough to matter.

Activity

  1. mrveiss commented on Aug 22, 2026

    @mrveiss
    OwnerAuthor

    Closed by #14837, squash-merged as 0703223db.

    Dev_new_gui is not the default branch, so closing keywords do not fire —
    closing by hand with evidence.

    Verified in base

    All 16 touched files are byte-identical between the branch tip and
    origin/Dev_new_gui (blob hashes, not commit reachability — a squash merge
    leaves the branch's own commits permanently unreachable either way). Behaviour
    confirmed present in base: _link_reporting_lines defined and called, the
    company-scoped UPDATE, and the two i18n keys in each locale.

    What the fix is

    execute_import pre-minted a destination UUID for every agent before any
    INSERT, then _import_agent decided per agent whether the row was created. Pass
    1 predicted the insert set and pass 2 decided it, and nothing reconciled the
    two — so a subordinate whose manager was skipped was written with the manager's
    pre-minted UUID, an id for a row that was never inserted. There is no FK on
    reports_to, so nothing rejected it, ever.

    A new pass 3 attaches reports_to only for managers that actually landed, and
    reports the rest as structured dropped_reporting_lines.

    Why this mattered more than a broken pointer

    /api/agents' org tree roots on reports_to IS NULL and matches children by
    agent_id, so an agent referencing a nonexistent manager is neither a root
    nor anyone's child
    — it disappeared from the tree entirely, could not be
    delegated to, and could not escalate, while the import reported success. The LLC
    org chart still rendered it, because that endpoint promotes an unresolvable
    parent to a root. That asymmetry is why this survived: the UI most people look
    at hid it.

    Mutation evidence

    The linker reproduced faithfully, then mutated to resolve against the pass-1
    prediction (the pre-fix behaviour), with the manager skipped:

    FIXED    updates={}                         no_dangling_update=PASS  loss_is_named=PASS
    PRE-FIX  updates={'dst-sub': 'dst-mgr'}     no_dangling_update=FAIL  loss_is_named=FAIL
    

    dst-mgr is precisely the pre-minted id of a row that was never inserted.

    Added in review

    The UPDATE is now scoped by company_id as well as agent_id. agent_id
    carries a table-wide UNIQUE constraint today so the clause is redundant — but
    that constraint is declared in a migration three files away, and the natural
    future change (making it unique per company, the first time two tenants want
    the same agent slug) would silently turn this into a cross-tenant rewrite. The
    test asserts the bound parameter, not the SQL text.

    CI

    59 checks passed, 0 failed, 12/12 python-suite shards, frontend suite green.

    The frontend check was red twice on unrelated tests before passing — a
    10s-timeout flake tracked as #14842, not a defect in this change. It was
    re-run with the reasoning posted publicly rather than retried silently.

    Follow-ups filed from this work

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

    backendbugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions