Scheduled refresh: bound the tagging step, say what it costs, and scan runs first - #12
Merged
Merged
Conversation
…n runs first
Implements docs/superpowers/specs/2026-08-23-scheduled-refresh-design.md, which
has sat at `status: proposed` since 2026-08-23. The failure it names was live:
`attest tag` in the generated refresh script was UNBOUNDED, so at the measured
2.3s/item a 1000-item backlog is a ~40-minute run against an hourly tick. With
`flock -n`, every subsequent wakeup then logs "SKIP: previous run still holding
lock" -- and the ingest half, the deterministic part that must succeed, never
runs either. A slow best-effort step starving a fast mandatory one.
BOUND IT. The script now calls `attest tag --limit "$TAG_BUDGET"`, computed
in-script from three named shell variables that each carry their measurement in
a comment rather than arriving as a bare integer:
TAG_SECONDS_PER_ITEM=2.3 TAG_BUDGET_MULTIPLIER=0.5 TAG_INTERVAL_SECONDS=3600
At hourly and 2.3s/item that is 782 items -- half the interval, so a run that
hits its budget still leaves the next tick a free lock. Untagged items are
picked up next pass, which is already how the script treats tag failures.
`REFRESH_INTERVAL_SECONDS` is new and `step_schedule` uses it too, so the
interval and the "17 * * * *" cron literal cannot drift apart.
SAY WHAT IT COSTS. `run_tagging` printed nothing until it finished, and a
40-minute silent command is the classic abandonment point. It now announces the
run to STDERR -- deliberately, because the refresh script redirects stdout to
/dev/null, so stderr is the only place a cron run's output survives.
The estimate is DERIVED, not the constant: `estimate_seconds_per_item` reads a
50-row trailing window of `item_features.tagged_at`, takes consecutive deltas,
and excludes any delta more than 10x the median as a boundary between two
separate RUNS rather than a slow item. That exclusion is the whole trick -- one
hour-long cron gap left in would swamp every genuine ~2s per-item delta.
MEASURED against the live database's 11,972 tagged rows: 2.558 s/item, close to
but independent of the hardcoded 2.3, which is the point (a 12B model on a
slower box is a different number). Edge cases verified: an injected hour-long
gap still yields 2.000, identical timestamps and a single row fall back to the
named constant rather than dividing by zero.
SCAN RUNS FIRST. `attest runs scan` is deterministic, needs no model, and
completes in ~1s on 1045 runs -- yet it was absent from the refresh while both
model-dependent steps were present. It now runs FIRST, ahead of ingest, so the
cheapest and most reliable step cannot be starved by anything after it, gated
on RESEARCH_ROOT being set and pointing at something that exists.
The spec's "must succeed" and "skip when unconfigured" are reconciled by making
the GATE the escape hatch rather than the exit code: unconfigured skips
silently, but configured-and-broken is fatal exactly like ingest, because it is
exactly as deterministic and model-free.
Spec flipped to `accepted` with both open questions resolved in the document.
The per-item estimate comes from `item_features.tagged_at` deltas and needs no
new schema -- the spec proposed `created_at`, which does not exist; the real
column is `tagged_at TEXT NOT NULL DEFAULT (datetime('now'))`, and the wording
is corrected. Prompting for RESEARCH_ROOT during install is DECLINED: commit
ddd560b exists precisely because `--check` reported BROKEN over optional wiring
a self-hoster never asked for, and a prompt for an optional env var repeats
that mistake in a different costume. Discoverability is still a real problem,
but a prompt is the wrong instrument for it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…sible
Three reviews independently called the run ledger the strongest thing in the
tool: it needs no model, completes in about a second on a real corpus, and is
the half of the two-tier promise the README leads with. It is also gated behind
an environment variable most users will never discover. Nothing in `attest
install --check` mentioned it, so a reader with a working feed had no way to
learn the capability existed.
[skipped] ledger: set RESEARCH_ROOT to a directory of projects to enable
`attest runs scan` (no model needed)
SKIPPED, and never BROKEN. This matters more than the wording. `--check` exits
nonzero iff some step is BROKEN, and ddd560b exists precisely because
`skill_copy` reported BROKEN over hermes-agent wiring a self-hoster never asked
for, forcing exit 1 on an install that was fine. An optional capability that
fails the doctor is that same mistake in a different costume -- so this line
informs and costs nothing: a user who only wants the feed still gets exit 0.
This is also why it is a report rather than the install-time prompt the
scheduled-refresh spec considered. That spec declined the prompt for the same
reason and recorded the decision; discoverability was the part it left open,
and naming the variable in a line that cannot nag is the instrument that fits.
Three states, each distinguishable so a reader can tell "never configured" from
"configured, now wrong":
unset -> [skipped] naming the variable and what it unlocks
real -> [ok] ledger: RESEARCH_ROOT=<path>
missing -> [skipped] ledger: RESEARCH_ROOT=<path> does not exist
A stale RESEARCH_ROOT from a moved workspace is a stale setting, not a broken
install, so it is SKIPPED too.
Reuses `ledger.workspace_root()` rather than reading the variable again --
that function already carries the "deliberately no default" reasoning, and a
second resolution path could disagree with the one `attest runs scan` uses.
Placed after first_data, the other purely-local step, and before the
model-dependent ones, because it belongs with the tier that works when Ollama
does not. It takes no `check` parameter: there is nothing here that could
mutate anything, and accepting a flag it would ignore would imply otherwise.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements
docs/superpowers/specs/2026-08-23-scheduled-refresh-design.md,which has sat at
status: proposedsince 2026-08-23, and flips it toacceptedwith both open questions resolved in the document.The failure it prevents was live
attest tagin the generated refresh script was unbounded. At the measured2.3s/item a 1000-item backlog is a ~40-minute run against an hourly tick — so
with
flock -n, every subsequent wakeup logsSKIP: previous run still holding lock, and the ingest half, the deterministic part that must succeed, neverruns either. A slow best-effort step starving a fast mandatory one.
Three changes
Bound the tagging step. The script now calls
attest tag --limit "$TAG_BUDGET", computed in-script from three named variables that each carrytheir measurement in a comment rather than arriving as a bare integer:
782 items at hourly — half the interval, so a run that hits its budget still
leaves the next tick a free lock.
REFRESH_INTERVAL_SECONDSis new andstep_scheduleuses it too, so the interval and the"17 * * * *"cron literalcannot drift apart.
Say what a run will cost, before it runs.
run_taggingprinted nothinguntil it finished. It now announces to stderr — deliberately, because the
refresh script redirects stdout to
/dev/null, so stderr is the only place acron run's output survives.
The estimate is derived, not the constant.
estimate_seconds_per_itemreads a50-row trailing window of
item_features.tagged_at, takes consecutive deltas,and excludes any delta more than 10x the median as a boundary between two
separate runs rather than a slow item. That exclusion is the whole trick: one
hour-long cron gap left in would swamp every genuine ~2s per-item delta.
MEASURED against the live database's 11,972 tagged rows: 2.558 s/item —
close to but independent of the hardcoded 2.3, which is the point, since a 12B
model on a slower box is a different number. Edge cases verified: an injected
hour-long gap still yields 2.000; identical timestamps and a single row fall
back to the named constant rather than dividing by zero.
Scan runs first.
attest runs scanis deterministic, needs no model, andcompletes in ~1s on 1045 runs — yet it was absent from the refresh while both
model-dependent steps were present. It now runs first, ahead of ingest, gated on
RESEARCH_ROOTbeing set and pointing at something that exists.The spec's "must succeed" and "skip when unconfigured" are reconciled by making
the gate the escape hatch rather than the exit code: unconfigured skips
silently, configured-and-broken is fatal exactly like ingest, because it is
exactly as deterministic and model-free.
Resolved open questions
item_features.tagged_atdeltas, no new schema.The spec proposed
created_at, which does not exist — the real column istagged_at TEXT NOT NULL DEFAULT (datetime('now')), and the wording iscorrected in the document.
RESEARCH_ROOTduring install. Declined. ddd560b existsprecisely because
--checkreported BROKEN over optional wiring a self-hosternever asked for; a prompt for an optional env var repeats that mistake in a
different costume. Discoverability is still a real problem — a line in
--checkoutput would inform without nagging — but that is a separate change.Verification
tests/test_install.py test_install_e2e.py test_install_skills.py test_features.py test_docs_site.py test_architecture.py→ 169 passed, 1 skippedruff format,ruff check,ty,uv.lock, complexity ratchet, bandit,xenon all Passed; the full-suite
pytesthook was killed by host memorypressure mid-run rather than by a failure, so the scoped run above stands in
tests/test_golden_paths.py::test_every_output_block_of_an_offline_path_is_realwas reported as a pre-existing failure during development. It is not: 13
passed on a clean tree and 13 passed with these changes applied. The
original failure was host load (15–49 load average), which is exactly the trap
docs/measurement-lessons.mdwarns about.🤖 Generated with Claude Code