`last_full_sweep` is a liveness heartbeat; stale threshold drops to 2h
Date: 2026-08-05 Status: closed Supersedes: none Superseded-by: none — current
Context
Section titled “Context”The petrova.host console reported 39 of 39 registered repos as stale-data,
every card showing no integration data and swept — stale. Investigation
established the sweep was alive and healthy the entire time: run 31017673968
completed success at 2026-08-05T14:55Z in 58s and resolved real data
(scanned=39 changed=4 skipped=4). The last commit that actually landed state
was 3eca387 on 2026-07-09 — 27 days of green runs producing zero writes.
Three defects combined:
sweepOne(cli/src/verbs/sweep_state.ts) returned early when the six semantic fields matched the prior file, skipping thewriteFileSync.last_full_sweepwas computed into the merged object and discarded. The field only ever advanced when something else changed — so it recorded “last time this repo moved”, while every consumer read it as “last time we looked”.- The workflow’s commit gate used
git diff --quiet -- state/, which does not see untracked files. The four repos with no state file on main (yao-agent,arno-host,bhava-blue,mary-wiki) were written into the runner workspace and discarded every 30 minutes. - Repos whose signals all resolve null never got a state file at all: with no
prior file the before/after comparison matched trivially and the early return
fired.
asgard,abacus,fathom,templatessat in this hole — neitherchangednorskipped, simply invisible.
Separately, the stale threshold was defined twice and inconsistently: 3 days in
dashboard/src/lib/fleet-state.ts (citing spec §2) and 24h in
host/src/tools/sweeps.ts. Spec §2 justifies 3 days on the premise that “the
state-sweep cron cadence is daily”. The cadence is and has been */30 * * * *.
Decision
Section titled “Decision”last_full_sweep is redefined as the fleet’s liveness heartbeat: it is
written on every successful sweep pass, whether or not anything changed. The
changed flag becomes report-only and no longer gates the write. Skip paths
(no url in registry, fetch failed: …) still write nothing, so a GitHub
outage cannot stamp a repo as freshly swept.
This makes state/ dirty on every run. That is accepted: the sweep commits
every run (~48/day, [skip ci], single-line diffs), and the commit log becomes
the sweep’s audit trail. The timestamp is deliberately not rounded or debounced
— doing so would reintroduce a resolution gap below the cron cadence.
STALE_THRESHOLD_MS is unified at 2 hours in a single definition
(host/src/freshness.ts), consumed by both the MCP tool and the console. 2h is
a tolerance on the 30-minute cadence that absorbs GitHub’s observed scheduling
deferrals (real gaps in the run history run ~2-3h) while surfacing a dead sweep
the same day. This supersedes the 3-day figure in spec §2.
Alternatives considered
Section titled “Alternatives considered”- Round
last_full_sweepto the hour so identical-content runs produce no diff — cuts commit volume ~16x but degrades heartbeat resolution below the 30-minute cadence, which is the precise signal being restored. - Write the file but keep committing only on semantic change — keeps
state/history clean and reintroduces the exact bug: main would still carry a frozen timestamp. - Add a separate heartbeat field and leave
last_full_sweepalone — a second freshness field every consumer must learn about, when the existing one was simply being written at the wrong time. - Keep 3 days / adopt 24h fleet-wide — both are forgiving enough that a fully stalled cron hides for a day or more, which is the failure this signal exists to catch.
Consequences
Section titled “Consequences”For code:
cli/src/verbs/sweep_state.ts— write on every non-skipped row;changedreports semantic movement only;role: placeholderentries get a distinct skip reason instead of reading as failures..github/workflows/state-sweep.yml— commit gate stages first (git add -A state/) then testsgit diff --cached --quiet, so new files are seen.cli/src/github-client.ts—parseRepoUrlaccepts dots in the repo name (devaqua.bluepreviously threwPARSE_REPO_URL), still rejecting deep paths.cli/src/types.ts—RepoRolegainsplaceholder, whichregistry.schema.jsonhas carried since the thrustr-io/downlink-hq/pwplz entries landed; the type had drifted behind the schema.host/src/freshness.ts(new) — sole definition ofSTALE_THRESHOLD_MS.host/src/tools/sweeps.ts,dashboard/src/lib/fleet-state.ts— consume it.
For docs:
docs/superpowers/specs/2026-05-16-fleet-lifecycle-verdict-design.md§2 gains a supersession pointer to this doc. The original text is left intact (MR-7).
For in-flight phases:
- None. The control plane’s self-entry carries
current_phase: null.
For invariants:
- No
MR-Nadditions, modifications, or repeals.
References
Section titled “References”- Spec:
docs/superpowers/specs/2026-05-16-fleet-lifecycle-verdict-design.md§2 (the superseded 3-day threshold and its “cadence is daily” premise). - Workflow run
31017673968— green sweep,scanned=39 changed=4 skipped=4,No state changes to commit. - Commit
3eca387(2026-07-09) — last sweep commit prior to this fix. MR-7— supersession via new dated doc, never silent edits.
Sign-off
Section titled “Sign-off”- Subagent: state-sweep rectification (this session)
- Human: alex@devarno.com — 2026-08-05