carlos / platform Public

spec: host lifecycle claim rev 5, CC15-CC19 from the strict joint model

At-rest sweep owns late completions; create registrations are leases drained
only when expired and unnamed by the claim; retired-incarnation records are
orphans whenever they land; holder.generation in the schema; staging writes
signed once at the deadline check with SafeUntil = Until - skew - 15m.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 4a458a9c82cc11aca46be4d55cbb5cfebd1209fc parent a3c86fa
1 file changed, +59 −8
  • docs/superpowers/specs/2026-10-02-host-lifecycle-claim-design.md +59 −8

CI passed — run details

diff --git a/docs/superpowers/specs/2026-10-02-host-lifecycle-claim-design.md b/docs/superpowers/specs/2026-10-02-host-lifecycle-claim-design.md
index 7bcf696ab..cb2597a7c 100644
--- a/docs/superpowers/specs/2026-10-02-host-lifecycle-claim-design.md
+++ b/docs/superpowers/specs/2026-10-02-host-lifecycle-claim-design.md
@@ -1,6 +1,6 @@
# The host lifecycle claim
-Design, 2026-10-03, **rev 4**. A small primitive cited by:
+Design, 2026-10-03, **rev 5**. A small primitive cited by:
- **#199**, deleting an instance and parking its data:
`2026-10-02-replica-ownership-design.md`, which this spec supersedes
@@ -17,7 +17,10 @@ I-7, adopts its replacement for D4, and answers N1, N3 to N10, N12 and the
carried-forward R3-12. The moves-only findings (N2's move side, N11, N13,
N14, N15) are the moves spec's. §12 maps every finding to its section.
-**Rev 4** answers the claim-side findings of joint round 2
+**Rev 5** folds in CC15 to CC19, required by the strict joint model
+(47e65af8, moves rev 6 at 7ac666fe).
+
+**Rev 4** answered the claim-side findings of joint round 2
(`CODEX-FINDINGS-JOINT-R2.md`, cited `J2-…`): R2-1, R2-2, R2-3, R2-4,
R2-6, R2-7, and the legacy local-coverage gap noted under N5.
@@ -71,7 +74,8 @@ replaced whole with conditional writes (`If-Match` on the ETag read).
"key": "apps/bab/jam/instances/shop.bab.oncarlos.com.json",
"account": "bab", "app": "jam", "name": "",
"incarnation": "<hex>", "created_at": "<record CreatedAt>",
- "location": "instances/shop.bab.oncarlos.com/<incarnation>"
+ "location": "instances/shop.bab.oncarlos.com/<incarnation>",
+ "generation": "<op id of the installed generation, or \"\">"
},
"op": {
"kind": "serving",
@@ -115,6 +119,10 @@ Field rules:
set and never changes while that holder holds the host. Same-incarnation
generations within it (moves, push) are the moves spec's, scoped by
operation id.
+- **`holder.generation`** names the installed generation within
+ `holder.location` (CC18). The moves spec owns its values and its
+ markers. Its start check compares this field with all three of its
+ markers.
- **`op.intent`** holds the operation's reconstruction inputs. It is
written in the **same CAS** that starts the operation, before any other
object is touched (J-N4).
@@ -209,10 +217,18 @@ in the serving region, or in a region the instance has left. So, for the
staging-work lease, **correctness does depend on the clock and pause
assumption**, and the rule is:
-- check the staging-work lease's `SafeUntil` immediately before **every
- request signature**, not only at the start of a round;
+- `SafeUntil = Until − max clock skew − 15 minutes`, where `Until` is the
+ lease's expiry (CC19);
+- **sign each staging request at the moment of the deadline check**, in
+ the same step, and send that signature. **Never re-sign**: the SDK's
+ automatic retry must not refresh the signature;
- never sign a staging write at or after `SafeUntil`.
+A box frozen between a passed check and the send then holds a signature
+S3 refuses once 15 minutes have passed. Because `SafeUntil` already
+subtracts those 15 minutes and the skew, any request S3 accepts was
+accepted before the lease expired.
+
Combined with S3's 15-minute signature-age limit (§13 row 8), a write
signed before `SafeUntil` either lands within that bound or is refused.
The staging cleanup's horizon (§6.2a) covers that bound. The moves spec
@@ -297,8 +313,10 @@ new operation will write (CC11). A pending chore there must finish first.
### 5.1 Create (R3-12, J-N4)
0. **App admission (J2-R2-2).** Register the create in
- `apps/<acct>/<app>/admission.json` with a CAS, as `{create id, host}`,
- which succeeds only while the app's admission is `open`. The
+ `apps/<acct>/<app>/admission.json` with a CAS, as `{create id, host,
+ expires}`, which succeeds only while the app's admission is `open`.
+ **A registration is a lease (CC16):** the creating console renews it
+ until the create reaches `serving` or is cancelled. The
registration is removed when the create reaches `serving` or is
cancelled.
1. The console assigns `CreatedAt` = now (UTC, nanoseconds). A caller
@@ -320,6 +338,13 @@ new operation will write (CC11). A pending chore there must finish first.
anyway is an **orphan**. It is never admitted (C1). The pass
deletes it with `DeleteIfMatch` when its incarnation matches a
`cancelled_creates` entry, and retries until `until` (J2-R2-7);
+ - **orphans whenever they land (CC17):** a record whose incarnation is
+ in `retired_incarnations`, or is not the claim's holder, is an
+ orphan at any time, not only inside the `cancelled_creates`
+ window. A create that finds its record key occupied by one
+ removes it with `DeleteIfMatch`, then retries its create-only
+ write. The pass removes any it meets. A late record write after
+ the window therefore never blocks a later recreate;
- anything else: wait.
### 5.2 Delete and park (#199)
@@ -369,7 +394,14 @@ site now uses:
host set is then the app's records **plus** every host still
registered as an in-flight create. Purge waits for each registered
create to reach `serving` (then it is purged like the others) or to be
- cancelled by recovery, before fixing its host set. This is tested with
+ cancelled by recovery, before fixing its host set. **The drain
+ removes a registration only when it has expired *and* its host's
+ claim does not name that create (CC16).** A claim that names it
+ means the create already holds the host, so purge handles that host
+ like any other. Expiry alone is not enough: dropping such a
+ registration would let purge tear down a store the create already
+ uses. A crashed console's registration therefore expires and drains,
+ and does not block every purge. This is tested with
at least two hosts.
1a. **Adopt first (CC6).** Run the one adoption routine (§9) for every
record of the app that has no claim, before deciding which hosts the
@@ -478,6 +510,20 @@ moment its stop evidence (§6.1) was established. Anything that listing
finds goes into a further retirement event of the same obligation, with
its own retirement id and manifest, and the verification repeats.
+**Requests that complete after `H` (CC15).** C3a makes a very late
+completion harmless to a successor, but it still leaves residue, and it
+needs an owner. Each region's box runs an **at-rest sweep**: for every
+host it can see in its bucket that has no operation running. It lists:
+
+- the location of every incarnation in that host's
+ `retired_incarnations`;
+- every generation within the live `holder.location` other than
+ `holder.generation` (the moves spec's dead generations).
+
+Anything found is parked as a further retirement event (§6.2), under a
+new obligation the sweeping box owns. A freed host's location never
+keeps late residue for good.
+
On acknowledging, the owner CASes the claim's `quiet_after[region]` to at
least `evidence time + H`. Any install into that region's host namespace
waits until `quiet_after` has passed: a create's first wake, a move's
@@ -766,6 +812,11 @@ the model catches it.
| J2-R2-6 | §6.2 |
| J2-R2-7 | §5.1 step 5, §2 `cancelled_creates` |
| J2 N5 note | §9 |
+| CC15 | §6.2a at-rest sweep |
+| CC16 | §5.1 step 0, §5.3 step 1b |
+| CC17 | §5.1 step 5 |
+| CC18 | §2 `holder.generation` |
+| CC19 | C3, the staging-work lease exception |
## 13. S3 behaviour this relies on, verified