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
Context
Section titled “Context”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 e6839ca — 39 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:
| File | Why it never refreshes |
|---|---|
state/asgard-codes.yaml | Orphan. 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.yaml | role: placeholder — no url, so sweepOne skips before any fetch. |
state/downlink-hq.yaml | same |
state/pwplz.yaml | same |
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.
Decision
Section titled “Decision”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.
Rejected alternative
Section titled “Rejected alternative”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.
Guard rails
Section titled “Guard rails”Deleting committed files from a scheduled job is the risk here. The prune runs only when all three hold:
- not a
--dry-run, - not a
--slugrun —--slugnarrowstargets, so every other slug would look orphaned and sweeping one repo would delete the fleet’s state, 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.
Consequences
Section titled “Consequences”- 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. SweepStateReportgains a requiredprunedfield. Onlycli/src/index.tsconsumes the report.- Consumers of
state/asgard-codes.yamlwould break — there are none; the file is all-null with noprobe_history, andasgardcarries the real data.
References
Section titled “References”docs/decisions/2026-08-05-sweep-heartbeat-and-stale-threshold.md— the heartbeat decision this extends, shipped as#211.- Workflow run
31074634157→ commite6839ca—39 file(s), 10 drift(s), the end-to-end proof that#211works. registry.yaml:678— theasgardentry that superseded theasgard-codesplaceholder.- 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.
Sign-off
Section titled “Sign-off”- Subagent: state-sweep rectification (this session)
- Human: alex@devarno.com — 2026-08-06