Patch release. One bug, found live against a real Gramps Web instance right after
v0.5.0 shipped: gramps_delete_all_objects failed on a cold client, which is
the normal case — a stdio MCP server is spawned per session.
The bug
Gramps Web rate-limits POST /api/token/ to one request per second per source IP
(@limiter.limit("1/second")). The wipe minted two tokens with only a single
metadata GET between them: the lazy login behind the precondition read, then an
unconditional fresh login for the endpoint's FreshProtectedResource. The second mint
came back 429 TOO MANY REQUESTS and the wipe died before the delete POST went out.
The tree was never touched by this failure — the 429 was raised from _login()'s
raise_for_status(), outside the try that guards the destructive POST. Verified
twice: by code order, and by a wire log.
The fix
- The fresh login before the POST is now conditional on this call not having minted
a token already.freshis a boolean JWT claim, not a time window, so a token minted
milliseconds earlier by the precondition read is already exactly what a second login
would produce. On a warm session the login still happens and still earns its mint:
Gramps Web checksis_tree_disabledonly at login, never per request, so it is the
last point at which the deployment can refuse. _loginrides out a single 429 and retries once, then raises the new
TokenRateLimitError. The limiter is keyed by IP, so a second MCP session or a
browser tab behind the same address can take the budget for the second this wipe needs
it — dropping our own second mint does not help there, only waiting does. Non-429
statuses still fail immediately and unretried: a 403 means the credentials are wrong,
and re-authenticating in a loop is what a brute-force guard counts.TokenRateLimitErrorsubclassesrequests.HTTPError, so the existing 502/503/504
fallthrough and every otherHTTPErrorhandler keep working. Its message scopes its
claim to the login itself rather than asserting nothing happened — it can surface
during the post-delete counts poll, where the wipe has already landed._delete_orphaned_notesno longer amplifies the retry. It swallows a per-note
transport error and carries on, which is right for a hiccup and wrong for a
client-wide rate limit: 20 orphaned notes meant 40 token POSTs and 22 s of blocking
sleep on the single-threaded stdio server, to fail anyway. It now breaks out on
TokenRateLimitErrorand reports what it managed to delete.
Verification
- 259 tests (243 → 259), 13 of the first 15 red before the fix — including one that
pinned the literal two-mint order (['login','request','login']). Mutation probe:
11 mutations, all red. - Adversarial review, 23 agents across 5 lenses: 17 findings, 16 refuted, 3 fixed here
(the retry wait was pinned by no test andTOKEN_RETRY_WAIT = 0.0left the suite
green; the orphan-note amplification above; aTokenRateLimitErrormessage that
claimed a retry had happened in the branch where none does). - Proven live against a real instance:
gramps_delete_all_objectsas the very
first call of a fresh session, no warm-up — 391 objects → 0 in 7.7 s, no 429; then
gramps_import_filerestored all 391 with identical handles andchangetimestamps.
Exactly the case that previously failed reproducibly. The instance uses
GRAMPSWEB_RATELIMIT_STORAGE_URI(Redis-backed limiter), so the test ran against the
strict configuration, not a lucky per-worker one.
No tool signatures changed; the tool count is unchanged at 27 + 4 destructive = 31.