ENG-524: Renewable-lease lifecycle + keepalive renew endpoint#3
Merged
clawhavenapp merged 4 commits intoJul 7, 2026
Conversation
Every host is now a renewable lease. POST /hosts without expires_at
stores now + LEASE_DEFAULT_TTL (new setting, default 86400s) instead of
null, so an abandoned host lapses and the janitor reaps it; an explicit
expires_at: null stays supported as the deliberate opt-in to a
permanent host. POST /hosts/{id}/renew is the keepalive: it bumps
expires_at to the given future instant, or by LEASE_DEFAULT_TTL from
now on an empty body. Only active and bootstrapping hosts renew;
unclaimed warm-pool members refuse with 409 (pool maintenance owns
them) and a missing host is 404.
Pool claims now always stamp the caller's resolved lease onto the
claimed host — the default lease, an explicit window, or null for a
deliberate permanent — replacing the warm-pool max-age TTL. Warming
itself still uses POOL_HOST_MAX_AGE_HOURS.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The janitor snapshots expired host ids, then deletes each one later under its own row lock — a renewal committing in that window returned 200 while the same janitor pass still tore the VM down. delete_host now takes expired_only (janitor-only, like pool_shed): if the locked read shows a live lease again, the host is spared. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two review findings. First, resolving the default lease up front put the full lease on the in-flight provisioning row, so an abandoned provision would leak until the lease instead of reaping after PROVISIONING_GRACE_SECONDS. The omitted-lease sentinel now travels to the points where a lease is actually stamped — the pool claim, or the post-provision TTL rewrite — so a default create keeps just the safety TTL while provisioning and the lease starts when the host is usable. Second, delete_host now returns whether it actually deleted the row; the janitor no longer logs or returns a renewal-race-spared host as reaped, and pool shedding no longer counts a claim-race-spared host as removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rface
Two review findings. First, the post-provision TTL rewrite stamped the
create-time lease unconditionally, discarding a renewal made while the
host was still bootstrapping (a client whose POST timed out can find
the row and renew it). The rewrite is now a guarded UPDATE on the
in-flight safety value, so newer intent wins.
Second, the renew route joins the black-box OpenAPI surface:
full-api.spec.js expects POST /hosts/{host_id}/renew and exercises it
against the live created host (auth, extend, 404).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
czpython
force-pushed
the
commonzenpython/eng-524-lease-renewal-renewable-lease-lifecycle-keepalive-endpoint
branch
from
July 7, 2026 17:18
d1e103f to
fe60209
Compare
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.
What
Turns the host lease into the core lifecycle primitive (ENG-524): everything is a renewable lease, safe by default, with an explicit permanent escape hatch.
LEASE_DEFAULT_TTL(seconds, default86400).POST /hostswithoutexpires_atnow storesnow + LEASE_DEFAULT_TTLinstead of null. An explicitexpires_at: nullstays supported as the deliberate opt-in to a permanent, never-reaped host — it is just no longer the accidental default.POST /hosts/{id}/renew(service-token auth). Body is either an explicit future tz-awareexpires_at(same validators as create, shared inhosts/schemas.py) or empty → extend byLEASE_DEFAULT_TTLfrom now. Returns the updated host. Onlyactiveandbootstrappinghosts renew; unclaimed warm-pool members 409 (pool maintenance owns them); missing host 404.HostService.get_or_create_host/create_host) distinguishes omitted-vs-explicit-null via anEllipsisTypesentinel default; the API route resolves the request shape frommodel_fields_set.POOL_HOST_MAX_AGE_HOURS.expires_at.Why
A long-lived home with
expires_at = nullwas never reaped — if the agent died silently the VM leaked and kept costing money. With renewal as the keepalive, a live agent keeps its home by bumping the window; a dead agent's home lapses and self-reaps. The default TTL protects naive self-hosters from surprise cloud bills.Acceptance criteria
POST /hostswith noexpires_atyields a host withexpires_at ≈ now + LEASE_DEFAULT_TTL.POST /hostswith explicitexpires_at: nullyields a permanent host (no expiry).POST /hosts/{id}/renewextends an active host; janitor no longer reaps it.expires_at).Codex review
ed9f8cd): the janitor snapshots expired ids, then deletes each under its own row lock — a renewal committing in that window returned 200 while the same janitor pass still tore the VM down.delete_hostnow takesexpired_only(janitor-only, mirroring thepool_shedguard): if the locked read shows a live lease again, the host is spared. Pinned bytest_janitor_delete_skips_a_host_renewed_in_the_race.842f82e): resolving the default lease up front put the full 24h lease on the in-flight provisioning row, so an abandoned provision would leak until the lease instead of reaping afterPROVISIONING_GRACE_SECONDS. The omitted-lease sentinel now travels to the points where a lease is actually stamped (pool claim, or the post-provision TTL rewrite); a default create keeps just the safety TTL in flight and the lease starts when the host becomes usable. Pinned bytest_create_host_keeps_safety_ttl_while_provisioning.842f82e):reap_expired_hostslogged and returned a renewal-race-spared host as reaped.delete_hostnow returns whether it actually deleted; the janitor reports only real reaps and pool shedding only counts real removals. Pinned bytest_reap_does_not_report_a_host_renewed_in_the_race.d1e103f): the post-provision TTL rewrite stamped the create-time lease unconditionally, discarding a renewal made while the host was stillbootstrapping. The rewrite is now a guarded UPDATE on the in-flight safety value, so newer intent wins. Pinned bytest_create_host_preserves_renewal_made_during_provisioning.d1e103f): the new route changes/openapi.json, whichapi-tests/tests/full-api.spec.jspins exactly. The expected operation list now includesPOST /hosts/{host_id}/renewand the black-box suite exercises it against the live created host (auth, extend, 404). The Playwright suite needs a live service + provider credentials, so it was not run locally.review-mr9xdgu0-cqy48d,review-mr9xuxoy-89z4o4,review-mr9yjnqm-d3lyid, all cancelled). Net review state: rounds 1–3 produced 5 findings, all fixed and test-pinned, nothing rebutted.Notes
test_pool_claim_preserves_pool_ttl_when_caller_omits_expires_atpinned the pre-lease behavior (omitted TTL → keep the pool max-age on claim). Its purpose — a claimed host must never leak forever — is now met more strongly by the default lease, so the test was rewritten to the new contract, plus a new test pinning explicit-permanent claims.uv run ruff check,uv run ruff format --check,uv run pyright(0 errors),uv run pytest(289 passed).🤖 Generated with Claude Code