carlos / platform Public

seeded spec rev 7: answer Astra round 6 (abort tears down the seed's own store before releasing the hostname)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 4361fb55c13f698dc485c83195ed10964c401a36 parent 0ef3e71
2 files changed, +62 −12
  • docs/superpowers/specs/2026-10-08-seeded-instances-design.md +47 −12
  • docs/superpowers/specs/CODEX-FINDINGS-RUN-AND-SEEDED-R6.md +15 −0

CI cancelled — superseded by a newer push — run details

diff --git a/docs/superpowers/specs/2026-10-08-seeded-instances-design.md b/docs/superpowers/specs/2026-10-08-seeded-instances-design.md
index 1a9b6b0af..cf0699235 100644
--- a/docs/superpowers/specs/2026-10-08-seeded-instances-design.md
+++ b/docs/superpowers/specs/2026-10-08-seeded-instances-design.md
@@ -1,6 +1,6 @@
# Instances that start from a database: `--from` (#260) and the import door (#261)
-**Status:** rev 6, 2026-10-08. Rev 3 answered round 2 (§12), rev 4 round 3 (§13), rev 5 round 4 (§14), and rev 6 round 5 (§15). Paul approved rev 1 (c66792a3). Rev 2
+**Status:** rev 7, 2026-10-08. Rev 3 answered round 2 (§12), rev 4 round 3 (§13), rev 5 round 4 (§14), rev 6 round 5 (§15), and rev 7 round 6 (§16). Paul approved rev 1 (c66792a3). Rev 2
answers Astra's round 1 (`CODEX-FINDINGS-RUN-AND-SEEDED-R1.md`; findings
8 to 20 are this spec's). Each change is marked **(codex N)**, and §11 lists
the dispositions.
@@ -115,6 +115,9 @@ type SeedSource struct {
| `--from` | `reserving` → `ready` → `placed` |
| import | `awaiting-upload` → `uploaded` → `ready` → `swapped` → `placed` → `committed` |
+Either kind can also be abandoned. Its record then goes `aborted` →
+`rolled-back`, and is deleted (§5.6).
+
An import can also leave the path:
- `refused`: the box would not swap; `Reason` says why.
@@ -126,7 +129,8 @@ other per-record handling:
| State | Reconcile |
|---|---|
-| `aborted` | runs the idempotent rollback (§5.6), or with no journal row deletes `seeds/<host>/<id>/` and the record |
+| `aborted` | runs the idempotent rollback (§5.6), or, with no journal row, deletes `seeds/<host>/<id>/` and CASes the record to `rolled-back` |
+| `rolled-back` | skips the record; the console finishes it (§5.6) |
| `reserving`, `awaiting-upload`, `uploaded`, `refused` | skips the record; nothing is minted |
| `ready` | mints (`--from`) or swaps (import) |
| `swapped`, `placed`, `committed` | ordinary handling, plus the import gate (§5.4) while not `committed` |
@@ -480,12 +484,29 @@ delete` is the verb then.
`DELETE …/instances/{host}` on a record whose seed is an import that isn't
`committed` answers 409, "abort the import instead". The only exceptions
are `awaiting-upload`, `uploaded` and `refused`, where no box has
-swapped, so the console deletes the record and `seeds/<host>/<id>/` itself.
-
-**The record is kept until the box has rolled back.** An `aborted` record
-stays in the bucket until the owning box deletes it at the end of the
-rollback below. A box that never swapped (there's no journal row for this
-seed `ID`) only deletes `seeds/<host>/<id>/` and the record.
+swapped, so the console moves the record straight to `rolled-back`, deletes
+`seeds/<host>/<id>/`, and finishes as below.
+
+**The record is kept until everything is clean.** An `aborted` record
+stays in the bucket while the box rolls back. The box then CASes it to
+`rolled-back`, never deleting it. A box that never swapped (there's no
+journal row for this seed `ID`) deletes `seeds/<host>/<id>/` and does the
+same CAS.
+
+**The console finishes** (codex R6-1). For a `rolled-back` record, the
+console's abort handler, or failing that its deadline sweep:
+
+1. **Tears down the private store this seed created**, if there is one.
+ The store record for the derived environment is torn down with the
+ existing `objstore.TeardownIn` (`walk.go:559-566`), but only if its
+ grant names this seed `ID` (§4 Storage). A store without that `ID` was
+ there before the seed, and is never touched.
+2. **Deletes the record** by CAS, if it still names this seed `ID`.
+
+The record holds the hostname until the store is gone, so a re-import of
+the same host can't meet the leftover store that §4 would refuse. The same
+finish runs for a `--from` that fails after creating its store, from its
+`aborted` record (§4 step 7).
**Rollback.** Reconcile runs it for any journal row whose import is
abandoned, meaning any of these:
@@ -537,14 +558,15 @@ journal's state says where to resume after a restart:
6. **Clean the bucket.** Delete the host's `instances/` replica, if any,
and `seeds/<host>/<id>/` for this seed `ID` only.
7. **Delete the journal row.**
-8. **Delete the record**, by CAS, if it still names this seed `ID`. It
- goes last (codex R4-1). Until then the aborted record holds the
+8. **CAS the record to `rolled-back`**, if it still names this seed `ID`.
+ The console finishes from there (store teardown, then record delete).
+ The record goes last of all (codex R4-1, R6-1). Until then it holds the
hostname, and `CreateRecord`'s create-only write refuses a new import or
`--from` for that host (`provision.go:845`). So no second seed can
arrive while this one's cleanup is still running. A crash after step 7
leaves an `aborted` record with no journal row, and §3.2's no-journal
- path (delete this seed's snapshot, then the record) finishes the job
- idempotently.
+ path (delete this seed's snapshot, then the CAS) finishes the box's
+ part idempotently.
**After a restart in `cleanup-pending`,** the box resumes at step 6. Its
deletes are scoped to this seed `ID`, and the record still holds the
@@ -785,6 +807,13 @@ into the code.
- on an app with private storage, `--environment` refused and an existing
store for the derived environment refused, except a retry of the same
seed `ID`; the source's store and bindings unchanged;
+ - abort and then re-import with private storage on, both before upload
+ and after the swap. The seed's store is torn down and the re-import
+ succeeds. A store whose grant names another seed `ID`, or none, is
+ never torn down. A crash between the teardown and the record delete
+ resumes;
+ - a failed `--from` after store provisioning: the store is torn down and
+ the host can be created again;
- restored size from the last LTX header, and the 413;
- the shared reservation pool across restores and spools, its 507, and
release on every path;
@@ -881,3 +910,9 @@ remaining imports wait for §3.4's claim algorithm.
| Finding | Disposition |
|---|---|
| R5-1 (P1) create doesn't guarantee an empty private store | **Accepted.** §4 Storage: with private storage, both seed doors refuse an explicit environment, use the derived one, and refuse an existing store unless it was created by the same seed `ID` |
+
+## 16. Round 6 dispositions
+
+| Finding | Disposition |
+|---|---|
+| R6-1 (P2) abort leaves a store that blocks re-import | **Accepted.** §5.6: the box ends at `rolled-back`; the console tears down the store only if its grant names this seed `ID`, then deletes the record, which holds the hostname until then. The same applies to a failed `--from` |
diff --git a/docs/superpowers/specs/CODEX-FINDINGS-RUN-AND-SEEDED-R6.md b/docs/superpowers/specs/CODEX-FINDINGS-RUN-AND-SEEDED-R6.md
new file mode 100644
index 000000000..be5cb346a
--- /dev/null
+++ b/docs/superpowers/specs/CODEX-FINDINGS-RUN-AND-SEEDED-R6.md
@@ -0,0 +1,15 @@
+| Round-5 finding | Disposition at `0ef3e715` |
+|---|---|
+| R5-1 — Creation does not guarantee an empty private store | **Fixed at spec level.** §4 applies the derived-environment rule to both doors, refuses existing stores, and permits reuse only when the grant names the same seed ID. |
+
+1. **P2 — Seeded §§4, 5.6: abort leaves a private store that blocks re-import.**
+
+ Begin import A for an app with private storage, then abort—even before uploading. Section 5.6 deletes the snapshot and instance record but leaves A’s store. Import B at the same hostname derives the same environment, receives a new seed ID, and is therefore refused by §4. The same problem follows a failed `--from` creation after store provisioning.
+
+ Storage provisioning is a separate operation ([instanceprivatestore.go:250](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/run-and-seeded/internal/console/instanceprivatestore.go:250)). Deleting the instance record removes only that object ([provision.go:1844](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/run-and-seeded/internal/provision/provision.go:1844)); removing an environment’s store requires the separate teardown operation ([walk.go:559](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/run-and-seeded/internal/objstore/walk.go:559)). Thus successful rollback does not restore the ability to retry the conversion.
+
+ **Fix:** assign durable cleanup of seed-created stores to the console’s abort recovery, including failures before swap. Guard cleanup by seed ID and binding ownership, preserve pre-existing stores and bindings, and retain the reservation until cleanup finishes. Test abort followed by re-import with private storage enabled.
+
+Seeded instances: **revise**.
+
+`carlos run`: **accept**, carried forward from round 4; not re-reviewed.
\ No newline at end of file