Skip to content

feat(orchestration): a post-sync action must say what change requires it #17454

Open

Description

@mrveiss

Parent: #17452
Depends on: #17453

Problem

The bar says an action is needed and never what needs it. Owner's words:

it just displays there is package update, not saying what is needing this update and how to do it

PostSyncAction (autobot-slm-backend/api/roles.py:108-116) carries role_name,
display_name, category, label, command, systemd_service. There is no field for the change
that triggered the action, and _classify_post_sync(role) takes only the role — it never sees what
the sync changed. So Install deps (AI Stack) cannot say "because requirements-ai.txt changed",
because at the point the action is built that fact is not in scope.

The information exists: sync_orchestrator knows which paths it synced for the role.

What "how to do it" needs

command is already carried and shown as the button's title, so the mechanism is one hover
away. What is missing is the cause, and the two are different questions: a reader who sees
venv/bin/pip install -r ... still cannot tell whether it is needed because a requirement changed,
because a migration is pending, or because the button is always shown.

Shape to decide

  • a trigger field (changed path, or requirement name, or a short reason string) — the minimum;
  • who fills it: the classifier cannot, so either the endpoint gains the sync's change set, or the
    action is built where that set is known;
  • whether an action with no known trigger renders differently from one with a trigger, so
    "no reason recorded" is visibly distinct from "no reason needed" — the nothing found vs did not
    look
    distinction, applied to a UI (see tech-debt(guards): 79 of 133 tree-scanning guards can report clean having examined nothing #15826).

Acceptance criteria

  • PostSyncAction carries the trigger for the action, populated from what the sync changed.
  • The badge or its tooltip names the trigger — a changed file, requirement, or pending
    migration — not only the command.
  • An action whose trigger is unknown is visibly distinct from one with a trigger; it does not
    render as though no reason were needed.
  • A test pins that an action built from a known change carries that change, and one built
    without a change set does not silently claim a trigger.

Activity

  1. mrveiss commented on Sep 25, 2026

    @mrveiss
    OwnerAuthor

    The owner's complaint is literally true at every layer, and the narrowest point is one bit wide

    Traced the whole chain before writing any code, because the shape question in the body ("who fills
    it") turned out to have an answer that changes this issue's blockers. All line numbers are
    origin/main; neither file below is touched by #17456.

    The signal is computed with the filename in hand and then collapsed to a boolean.

    1. autobot-slm-backend/api/code_sync.py:4552 — the watched set is known and small:
      _DEPS_FILES = ["requirements.txt", "requirements-ai.txt", "package-lock.json"]
    2. _check_deps_changed(code_source_dir, deployed_dir, dep_file) -> bool compares hashes per
      file
      , so at that moment the specific file is in scope.
    3. autobot-slm-backend/api/code_sync.py:4699 — _compute_deps_changed(component) -> bool loops
      over that set and return True on the first hit. The filename is discarded here. Note it also
      stops looking, so even widened to a list it could not report the second and third file
      without losing the early exit.
    4. autobot-slm-backend/models/database.py:390 — the durable record is
      deps_changed = Column(Boolean, default=False). One bit per component sync job.
    5. autobot-slm-backend/api/roles.py:111-119 — PostSyncAction has no trigger field, and
      _classify_post_sync(role) receives only the Role.

    So the owner's sentence — "it just displays there is package update, not saying what is needing
    this update"
    — is not a UI gap. deps_changed is the string "there is a package update",
    stored at the narrowest width a fact can have. The frontend renders faithfully what the backend
    chose to remember.

    Consequence for this issue's blockers

    The declared Depends on: #17453 is real but incidental — #17453 only orders the bar. AC1 is
    blocked on #17455
    , which owns the durable record. Nothing in the system currently persists which
    file changed, so there is no source an endpoint could read:

    • component_sync_jobs carries deps_changed (bool), post_steps (newline-joined log),
      component, status, success, message.
    • fleet_sync_node_states carries no changed-path field at all.
    • Role.source_paths exists, but it is the role's static sync scope, not what a given sync
      changed. Populating trigger from it would satisfy the field's type and not its meaning — a
      plausible-looking value in the right shape, which is the failure mode this repo treats as more
      serious than an empty one.

    What this implies for AC3, and a third state the body did not anticipate

    AC3 asks that an unknown trigger be visibly distinct from one with a trigger. Once the data source
    is deps_changed, there are three states, not two:

    State Meaning Available
    a named trigger "because requirements-ai.txt changed" needs #17455 + widening step 3
    known-that, unknown-which deps_changed=True, file discarded today
    nothing recorded no sync job, or default=False never written today

    The middle row is the one that matters and it is the current production state for every action.
    Rendering it as either of the outer two would be wrong in opposite directions: as a named trigger
    it fabricates, as nothing-recorded it hides a fact the system does have. So AC3 needs three
    renderings, and default=False means absent and false are indistinguishable in the existing column
    — a widening of this field cannot be a boolean with a nullable sibling without carrying that
    ambiguity forward.

    Scope, stated rather than assumed

    Steps 3-4 are code_sync.py and models/database.py — backend/orchestrator, a different file set
    from #17456 (roles.py, _post_sync_plan.py, PostSyncActionBadges.vue). Under same file ⇒ one
    PR, one agent
    the widening does not belong in #17456, and it is outside the frontend scope this
    session is holding. AC2/AC3/AC4 are frontend and are mine, but they are not worth building against
    a field that is always the middle row — a badge that says "something changed" is the defect
    restated in a tooltip.

    Recommendation: #17455 widens the record (returning and persisting the changed dep files, keeping
    absent distinguishable from none), and this issue lands after it. Not filing that as new work —
    #17455 already owns it; adding the blocked_by edge instead.

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

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions