Skip to content

The state sweep prunes state files no registry entry can refresh

Date: 2026-08-06 Status: closed Supersedes: none — extends 2026-08-05-sweep-heartbeat-and-stale-threshold.md Superseded-by: none — current

2026-08-05-sweep-heartbeat-and-stale-threshold.md made last_full_sweep a liveness heartbeat and landed as #211. Verified end-to-end: sweep run 31074634157 committed e6839ca39 file(s), 10 drift(s) — where the old git diff gate had seen 27.

Four repos still rendered stale-data afterwards. isStale (dashboard/src/lib/fleet-state.ts:32) treats a null lastSweepIso as stale, and four state files can never receive a heartbeat:

FileWhy it never refreshes
state/asgard-codes.yamlOrphan. The asgard-codes org-level placeholder was promoted to slug asgard (registry.yaml:678) in the 2026-05-13 promotion. The state file outlived the slug; nothing in the registry names it.
state/thrustr-io.yamlrole: placeholder — no url, so sweepOne skips before any fetch.
state/downlink-hq.yamlsame
state/pwplz.yamlsame

All four date to the #67 baseline stubs and have been null-heartbeat since. They were pre-existing, not created by #211 — but #211’s git add -A state/ made their permanence visible by finally committing the full state directory.

sweepState prunes any state/<slug>.yaml whose slug is not a registry entry with a url, plus that slug’s derived state/transitions/<slug>.jsonl. One rule covers both orphans and placeholders: the predicate is “can a future sweep refresh this file”, and for both categories the answer is no.

Placeholders keep their registry entry and keep appearing in the sweep report as skipped: placeholder — no repo yet. Only the unrefreshable state file goes. A placeholder that later gains a url gets its file written on the very next pass, so this is not a one-way door.

Exempting role: placeholder from isStale in the console. That amends the spec §2 verdict definition under MR-7, spreads registry knowledge into the console, and still leaves the asgard-codes orphan — which has no registry entry at all and therefore no role to exempt. Pruning fixes both classes in the data layer and leaves the verdict logic untouched.

Deleting committed files from a scheduled job is the risk here. The prune runs only when all three hold:

  1. not a --dry-run,
  2. not a --slug run — --slug narrows targets, so every other slug would look orphaned and sweeping one repo would delete the fleet’s state,
  3. registry.repos.length > 0 — a registry that parsed to nothing must never read as “no slug is sweepable”.

Each prune is named individually in the workflow log and in the CLI’s human output; the sweep report carries a pruned: string[] field. Silent deletion inside a bulk state commit is exactly what this must not become.

  • 40 state files → 36, matching the 36 registry slugs that carry a url. Every remaining file has a live heartbeat, so the console’s stale count becomes a pure signal of “the sweep is late” and nothing else.
  • SweepStateReport gains a required pruned field. Only cli/src/index.ts consumes the report.
  • Consumers of state/asgard-codes.yaml would break — there are none; the file is all-null with no probe_history, and asgard carries the real data.
  • docs/decisions/2026-08-05-sweep-heartbeat-and-stale-threshold.md — the heartbeat decision this extends, shipped as #211.
  • Workflow run 31074634157 → commit e6839ca39 file(s), 10 drift(s), the end-to-end proof that #211 works.
  • registry.yaml:678 — the asgard entry that superseded the asgard-codes placeholder.
  • Commit 11715cc (#67) — where all four stub state files originate.
  • MR-7 — extension via new dated doc, never a silent edit to the 2026-08-05 decision.
  • Subagent: state-sweep rectification (this session)
  • Human: alex@devarno.com — 2026-08-06