Skip to content

deploy: the builtin updater never removes files deleted from source, so deleted modules pile up on every host #16310

Description

@mrveiss

Measured 11 Sep 2026 with the SLM's own File Drift Check on the maintainer's install, then classified by git history (read-only). No drift and no manual patches in any of the four components checked. The owner's rule is that there must never be manual patches, so every host file has to come from, and be cleaned up by, the builtin updater. The untracked files it lists show the updater never removes a file that was deleted from source.

What the drift check lists as "untracked on this host"

component untracked what git history says
autobot-backend 11 10 deleted from source between 2026-08-17 and 09-05, including the code module security/enterprise/config_loading.py (3fc18e7) and utils/semantic_chunker_gpu_optimized.py (20036f9), both still importable on the host, plus 8 test files. 1 never tracked: a nested autobot-backend/autobot-backend/config/npu_workers.yaml, byte-identical (same sha256) to the canonical config/npu_workers.yaml. It was written on 2026-07-30 by the old cwd-relative NPUWorkerManager default, since fixed (see tests/test_npu_config_path_not_cwd_relative.py).
autobot-frontend 1 src/components/settings/DevicePairingSettingsPanel.vue, deleted from source 2026-08-10 (91ed3bc)
autobot-slm-backend 5 all deleted from source: the whole ansible/roles/python314/ role (de83f71, 09-03), ansible/tests/inventory/group_vars/all.yml (4b6defc, 08-21) and services/reconciler_flap_escalation_test.py (3cf6918, 08-18)
autobot-slm-frontend 345 src/views/ServicesView.vue, deleted 09-01 (c815c7b). Three dist-<build-id>/ bundles, which are expected: services/slm_frontend_build.py keeps the newest SLM_FRONTEND_RELEASE_KEEP (default 3). And the legacy dist.previous/ (9.2 MB) from the pre-#15462 publish design, which the pruner never matches because its name lacks the dist- prefix.

Why they stay: a normal update syncs without deletion. Only a drift resolve is a delete-style rsync (code_sync.py, #13913/#13851), and that deletes every untracked file, host state and build bundles included. So neither path can safely clean this up, and deleted modules pile up on every host.

Acceptance criteria

  • A normal code-sync removes, from the deployed tree, every path tracked at the previously deployed commit and deleted or renamed away at the new one (git diff --name-status --diff-filter=DR <old>..<new> -- <component>). It never touches a path git has never tracked, so runtime and host state are safe by construction. Test on a fixture repo: a deleted file is removed, a renamed file's old path is removed, and a never-tracked file survives.
  • The drift check labels each untracked file: removed from source at <commit> (the next sync removes it), build bundle (expected, with retention), or never tracked (host or runtime state, left alone). An operator can tell debris from state without reading git.
  • The legacy dist.previous/ is removed once the current/previous symlink layout is in place, through the SLM frontend publish step, not by hand.
  • The nested npu_workers.yaml duplicate is removed through the updater, as a leftover of the fixed cwd bug, only while it is byte-identical to the canonical file.
  • Host evidence: after one update through the GUI, the drift check shows 0 "removed from source" files on all four components.
  • The bootstrap plan is computed against the full history (deepen or unshallow the updater clone for this computation, or keep code_source at full depth). A bootstrap computed on a shallow clone is not recorded as complete, so the next update re-runs it, including on hosts that already hold a marker from an empty shallow bootstrap.

Relates to #13947, #13851, #13913, #15462, #12526.

Activity

  1. mrveiss commented on Sep 11, 2026

    @mrveiss
    OwnerAuthor

    Owner requirement, 11 Sep 2026: "We need to have drift check for all the files." Folded into this issue's scope, read as:

    • Every file under each deployed component is checked. Untracked files are no longer "not drift": a file removed from source is drift, and only declared host state and expected build bundles are excluded.
    • Every exclusion is named and counted in the result: the host-state set, build bundles, the venv, node_modules and the rest. "Clean" can never hide "did not look".
    • One run covers every component, plus autobot_shared and the infrastructure tree, not one component at a time.
    • Tests: a planted file removed from source, a planted never-tracked host file, and a planted modified file each get the right verdict, and a run over an empty or unreadable tree fails loudly rather than reporting no drift.
  2. mrveiss commented on Sep 11, 2026

    @mrveiss
    OwnerAuthor

    Owner decisions, 2026-09-11 (after a verification review found AC5 unreachable with the Python-side wiring). The normal Update All fires the full playbook, which restarts the SLM partway through, so Python never sees a success to hook onto. Three components also have no marker to start from, which made the pass a permanent no-op for them.

    1. Deletion runs in the Ansible roles. Before the update, the SLM computes a safe deletion list per component: renames and deletions between the last-deployed commit and the new one, excluding host-state files, gitignored files and build bundles, and contained to the deployed tree. Each role deletes that list right after its own file sync succeeds, then records the new commit in the pass's own marker, never .deployed_commit. That's one mechanism for co-located and remote nodes alike, so deploy: ansible synchronize never deletes files removed from source on remote fleet nodes #16322 folds into the same PR.
    2. A one-time cleanup where no record exists. For a component with no deletion marker and no legacy marker (backend, frontend and SLM-frontend today), the first update deletes the files the drift check proves git once tracked and no longer tracks, with the same exclusions, then starts recording. It runs at most once per component. This relies on the owner's no-manual-patches rule, which treats such a file as debris. The residual risk is a file deleted from git long ago and put back on the host by some other means; it's named in the code and the tests.

    AC5 still needs host evidence from a real update through the GUI.

  3. 18 remaining items

  4. mrveiss commented on Sep 14, 2026

    @mrveiss
    OwnerAuthor

    Evidence from the live install on 2026-09-14: the one-time bootstrap deleted nothing, because the updater's code_source clone is shallow.

    • The first update after fix(code-sync): delete files removed from source through the updater, and check drift across every file (#16310, #16322) #16351 merged (18:32, Riga) had no marker for autobot-backend, so it ran the bootstrap path: enumerate present files, compute the bootstrap plan, settle on the plan. No delete step ran, and it then wrote the marker. The next update (19:31) used the diff-based plan from that marker.
    • Twelve files deleted from source in August are still deployed under autobot-backend/, including llm_shared/semantic_cache.py, utils/semantic_chunker_gpu_optimized.py and security/enterprise/config_loading.py. Only the nested npu_workers.yaml went, through its own dedicated task.
    • Root cause, checked read-only: code_source has rev-parse --is-shallow-repository = true, only 1 commit reachable, and 210 shallow-boundary entries, against 13,203 commits on origin/main. _ever_added_paths runs git log -M --name-status --diff-filter=AR there and gets nothing back for these files. So _bootstrap_candidates (services/sync_deletions.py) finds no candidates and the plan comes out empty. It isn't kept_reason: none of the files are host-state or gitignored.
    • Because the marker was written after that empty bootstrap, later runs use diff-based mode and never revisit these files. Without a fix they stay on the host permanently.

    Added criterion:

    • The bootstrap plan is computed against the full history (deepen or unshallow the updater clone for this computation, or keep code_source at full depth). A bootstrap computed on a shallow clone is not recorded as complete, so the next update re-runs it, including on hosts that already hold a marker from an empty shallow bootstrap. Verified on a host: after one update, the 12 August leftovers are gone and the drift check shows 0 "removed from source" files for autobot-backend.
  5. added a commit that references this issue on Sep 16, 2026
  6. mrveiss commented on Sep 16, 2026

    @mrveiss
    OwnerAuthor

    #16789 merged (e49d3d077) — correctly left open, and one criterion cannot close without host evidence

    PR #16789 landed on main as e49d3d077 carrying Refs #16310, which is the right keyword: it
    contributes to this issue without completing it. Recording where that leaves the criteria.

    Criterion 5 cannot be ticked from any diff: "Host evidence: after one update through the GUI, the
    drift check shows 0 'removed from source' files on all four components."
    That is a statement about a
    running system after an operation performed through the maintenance UI. No amount of reading merged
    code satisfies it — it needs the update run and the drift check read afterwards. Flagging it explicitly
    so nobody ticks it on the strength of the code looking right.

    The other five are diff-verifiable but unverified. I merged this PR and have not checked them, so
    none should be ticked yet. The two worth particular care:

    • Criterion 1 requires the deletion set come from git diff --name-status --diff-filter=DR <old>..<new>
      and that a never-tracked path survives. The test called for is three-way — deleted file removed,
      renamed file's old path removed, never-tracked file untouched — and a test that only proves the first
      two would pass while leaving the dangerous case unexercised.
    • Criterion 6 concerns a bootstrap computed against full history, with the specific failure that a
      bootstrap computed on a shallow clone must not be recorded as complete. A marker written from an empty
      shallow bootstrap is indistinguishable from a successful one unless something checks depth — the same
      shape as a guard that cannot tell nothing found from did not look.

    Next step: whoever verifies should check each criterion against e49d3d077 and tick only what the
    merged code demonstrates, leaving criterion 5 unticked with a note until a host run exists. Partial
    delivery closes nothing; this issue stays open until all six hold.

  7. mrveiss commented on Sep 19, 2026

    @mrveiss
    OwnerAuthor

    Criterion 5 host evidence: NOT MET. Collected read-only from the live install on 2026-09-19.

    • Every component (backend, frontend, SLM frontend and backend, browser worker, NPU worker, AI stack) now has its own deletion marker, current as of today's source commit. The deletion pass is wired and running for all of them, including the 3 that previously had no marker.
    • But 3 of the files this issue's 2026-09-14 comment listed as August leftovers are still deployed under the backend and are absent from source: the llm_shared semantic-cache helper, the GPU-optimised chunker util, and the enterprise config-loading module.
    • Root cause, still present: the updater's source checkout is still a shallow clone (is-shallow-repository returns true).
    • Why it can't self-heal: sync_deletions now correctly refuses to bootstrap on a shallow clone. Every component's marker, however, was written before that guard existed, so each one sits in diff-only mode permanently. The bootstrap sweep that would find these files never runs again.

    The fix this still needs, in the builtin updater and not by hand on the host: the updater has to guarantee full history for its source checkout (unshallow it on fetch). It also has to re-run the bootstrap for any component whose marker predates the shallow-clone guard, or was written on a shallow clone.

  8. mrveiss commented on Sep 28, 2026

    @mrveiss
    OwnerAuthor

    Chunking triage — a proposal, not an assignment

    Nothing was relabelled, moved or closed by this pass.

  9. modified the milestones: v0.9.0, v0.9-umbrellas on Sep 28, 2026
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