carlos / platform Public

db-region-forward spec rev 5 and round 5 brief: Astra round 4 fixes

Four direct fixes, no new mechanism. 19: every progress line is flushed
with ResponseController.Flush, the headers leave with the first line, the
ndjson type is not gzipped by the edge, and work before the 200 is bounded
for backhaul's 30 s header timeout. 20: the console reads result.json
before listing chunks and writes the result line only after every chunk
through last_seq. 8: restart reconciliation by refusal against running
units, and a worker that loses its connection exits within 5 s since the
file replica client and LTX compactor ignore cancellation. 7: plaintext
limit and stored ciphertext separated. The brief asks for the disposition
table and new P0 to P2 findings only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 7f463d84eac2d2e161a1a5ade8c5efdbde342df6 parent 6d4a17d
2 files changed, +122 −97
  • docs/superpowers/specs/2026-10-22-db-region-forward-design.md +94 −22
  • docs/superpowers/specs/CODEX-REVIEW-BRIEF-DB-REGION-FORWARD.md +28 −75

CI passed — run details

CI log
=== gate ===
.amadan/test-gate.sh
=== launchctx (1 s)
ok  	amadan.net/carlos/platform.git/launchctx	0.011s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
=== vet (5 s)
=== gofmt (0 s)
gofmt -l .: clean
=== rest (151 s)
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.978s
ok  	github.com/carlosframework/platform/cmd/moveprobe	11.565s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.705s
ok  	github.com/carlosframework/platform/internal/activator	59.206s
ok  	github.com/carlosframework/platform/internal/agentsock	0.017s
ok  	github.com/carlosframework/platform/internal/alerting	0.004s
ok  	github.com/carlosframework/platform/internal/analytics	0.020s
ok  	github.com/carlosframework/platform/internal/appconfig	2.926s
ok  	github.com/carlosframework/platform/internal/apperr	0.075s
ok  	github.com/carlosframework/platform/internal/applog	0.140s
ok  	github.com/carlosframework/platform/internal/awsinv	0.005s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.006s
ok  	github.com/carlosframework/platform/internal/boxid	0.548s
?   	github.com/carlosframework/platform/internal/boxid/boxidtest	[no test files]
?   	github.com/carlosframework/platform/internal/boxid/internal/signtest	[no test files]
ok  	github.com/carlosframework/platform/internal/boxvolumes	0.010s
ok  	github.com/carlosframework/platform/internal/caps	0.013s
ok  	github.com/carlosframework/platform/internal/certwarm	0.003s
ok  	github.com/carlosframework/platform/internal/cgroup	0.710s
ok  	github.com/carlosframework/platform/internal/changelog	0.358s
ok  	github.com/carlosframework/platform/internal/claimactivation	0.915s
ok  	github.com/carlosframework/platform/internal/clamd	0.054s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
?   	github.com/carlosframework/platform/internal/controlname	[no test files]
?   	github.com/carlosframework/platform/internal/controlsock	[no test files]
ok  	github.com/carlosframework/platform/internal/dbdoor	6.491s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.010s
ok  	github.com/carlosframework/platform/internal/dbverify	0.022s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.020s
ok  	github.com/carlosframework/platform/internal/docsite	0.024s
ok  	github.com/carlosframework/platform/internal/domains	2.820s
ok  	github.com/carlosframework/platform/internal/doorkey	0.026s
ok  	github.com/carlosframework/platform/internal/economics	0.021s
ok  	github.com/carlosframework/platform/internal/edge	45.489s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.008s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.135s
ok  	github.com/carlosframework/platform/internal/gate	1.841s
ok  	github.com/carlosframework/platform/internal/genselect	0.016s
ok  	github.com/carlosframework/platform/internal/geo	0.015s
ok  	github.com/carlosframework/platform/internal/geoip	0.030s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.157s
ok  	github.com/carlosframework/platform/internal/host	140.861s
ok  	github.com/carlosframework/platform/internal/hostactors	5.990s
ok  	github.com/carlosframework/platform/internal/hostactors/awsmembership	0.009s
ok  	github.com/carlosframework/platform/internal/hostbox	3.368s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.970s
ok  	github.com/carlosframework/platform/internal/hostenv	0.007s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.008s
ok  	github.com/carlosframework/platform/internal/hostexec	5.784s
ok  	github.com/carlosframework/platform/internal/hostfence	0.053s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.366s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.021s
ok  	github.com/carlosframework/platform/internal/hostguard	1.065s
ok  	github.com/carlosframework/platform/internal/hostops	2.203s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.005s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.006s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.783s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.238s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.006s
ok  	github.com/carlosframework/platform/internal/infraview	0.004s
ok  	github.com/carlosframework/platform/internal/instancectl	5.253s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.884s
ok  	github.com/carlosframework/platform/internal/instancefs	0.006s
ok  	github.com/carlosframework/platform/internal/instancekey	0.004s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.015s
ok  	github.com/carlosframework/platform/internal/layout	2.027s
ok  	github.com/carlosframework/platform/internal/lbhealth	0.011s
ok  	github.com/carlosframework/platform/internal/lease	10.109s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.629s
ok  	github.com/carlosframework/platform/internal/ledger	0.036s
ok  	github.com/carlosframework/platform/internal/mail	0.308s
ok  	github.com/carlosframework/platform/internal/maintenance	0.147s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.595s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.126s
ok  	github.com/carlosframework/platform/internal/metrics	0.010s
ok  	github.com/carlosframework/platform/internal/moves	21.483s
ok  	github.com/carlosframework/platform/internal/moves/archive	1.154s
ok  	github.com/carlosframework/platform/internal/objstore	0.303s
ok  	github.com/carlosframework/platform/internal/ops	0.236s
ok  	github.com/carlosframework/platform/internal/park	0.340s
ok  	github.com/carlosframework/platform/internal/plans	0.006s
ok  	github.com/carlosframework/platform/internal/privatefile	0.003s
ok  	github.com/carlosframework/platform/internal/provision	14.545s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.010s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	5.739s
ok  	github.com/carlosframework/platform/internal/relay	7.249s
ok  	github.com/carlosframework/platform/internal/release	25.766s
ok  	github.com/carlosframework/platform/internal/rollout	3.918s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.012s
ok  	github.com/carlosframework/platform/internal/rootaws	0.008s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.080s
ok  	github.com/carlosframework/platform/internal/rootstate	0.031s
ok  	github.com/carlosframework/platform/internal/runs	0.583s
ok  	github.com/carlosframework/platform/internal/s3compat	0.009s
ok  	github.com/carlosframework/platform/internal/safefile	0.005s
ok  	github.com/carlosframework/platform/internal/schedule	2.811s
ok  	github.com/carlosframework/platform/internal/seeds	0.013s
ok  	github.com/carlosframework/platform/internal/sim/engines	2.843s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	11.188s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.736s
ok  	github.com/carlosframework/platform/internal/status	0.064s
ok  	github.com/carlosframework/platform/internal/steering	0.011s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.235s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.005s
ok  	github.com/carlosframework/platform/internal/termfence	0.005s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.004s
ok  	github.com/carlosframework/platform/internal/unixdial	0.003s
ok  	github.com/carlosframework/platform/internal/updatewords	0.006s
ok  	github.com/carlosframework/platform/internal/usage	0.005s
ok  	github.com/carlosframework/platform/internal/version	0.002s
ok  	github.com/carlosframework/platform/internal/vet	1.597s
=== internal_console@0 (82 s)
ok  	github.com/carlosframework/platform/internal/console	79.553s
=== internal_console@1 (35 s)
ok  	github.com/carlosframework/platform/internal/console	32.420s
=== internal_console@2 (41 s)
ok  	github.com/carlosframework/platform/internal/console	38.803s
=== internal_console@3 (37 s)
ok  	github.com/carlosframework/platform/internal/console	34.591s
=== internal_console@4 (38 s)
ok  	github.com/carlosframework/platform/internal/console	36.144s
=== internal_console@5 (40 s)
ok  	github.com/carlosframework/platform/internal/console	37.134s
=== internal_console@6 (42 s)
ok  	github.com/carlosframework/platform/internal/console	39.988s
=== internal_console@7 (43 s)
ok  	github.com/carlosframework/platform/internal/console	40.613s
=== root@0 (32 s)
ok  	github.com/carlosframework/platform	30.719s
=== root@1 (44 s)
ok  	github.com/carlosframework/platform	42.368s
=== root@2 (41 s)
ok  	github.com/carlosframework/platform	39.748s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
linux/arm64 carlos binary: 50069666 bytes (ceiling 52000000)
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
  minio: cached, digest verified
  mc: cached, digest verified
  minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
  buckets and scoped user ready
=== RUN   TestDeliveredContractMinio
=== RUN   TestDeliveredContractMinio/put
=== RUN   TestDeliveredContractMinio/get_round-trips_the_body
=== RUN   TestDeliveredContractMinio/list_shows_the_key
=== RUN   TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN   TestDeliveredContractMinio/delete_removes_it
=== RUN   TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.03s)
    --- PASS: TestDeliveredContractMinio/put (0.00s)
    --- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
    --- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
    --- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.02s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
    --- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
    --- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok  	github.com/carlosframework/platform/internal/objstore	0.037s
=== RUN   TestT4StorageProbeMinio
2026/10/22 12:09:13 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18E0D8BBECF559DD, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok  	github.com/carlosframework/platform/internal/host	0.033s
minio contract: PASS
diff --git a/docs/superpowers/specs/2026-10-22-db-region-forward-design.md b/docs/superpowers/specs/2026-10-22-db-region-forward-design.md
index 3ae989ec8..22cf1ffef 100644
--- a/docs/superpowers/specs/2026-10-22-db-region-forward-design.md
+++ b/docs/superpowers/specs/2026-10-22-db-region-forward-design.md
@@ -1,9 +1,14 @@
# `carlos db` for instances whose data lives in another region
-**Status:** rev 4, 2026-10-22. Design only, nothing built. Written for Paul,
+**Status:** rev 5, 2026-10-22. Rev 5 answers Astra's round 4
+(`CODEX-FINDINGS-DB-REGION-FORWARD-R4.md`), which found J1 to J5 fixed, with
+four direct fixes and no new mechanism: flushed progress lines and the
+backhaul header bound (codex 19), the result held until its chunks are out
+(codex 20), restart reconciliation by refusal and a 5 s worker exit (codex
+8), and the retention size wording (codex 7). Design only, nothing built. Written for Paul,
who approved the direction (§2). Review brief:
`CODEX-REVIEW-BRIEF-DB-REGION-FORWARD.md`. Rev 1 was `b35b3ab1`, rev 2
-`2cf4a88f`, rev 3 `944a61fd`. Rev 4 answers Astra's round 3
+`2cf4a88f`, rev 3 `944a61fd`, rev 4 `c07629e4`. Rev 4 answered Astra's round 3
(`CODEX-FINDINGS-DB-REGION-FORWARD-R3.md`) by making a forwarded read one
synchronous HTTP request (§5.2): no follow, no cancel route, nothing the
console writes after the request ends, and the store's lifecycle rule as
@@ -197,7 +202,7 @@ The longest a read can hold the request is the pickup window (2 minutes
with skew) plus the owner's deadline (20 minutes), so 22 minutes, and the
CLI's 23-minute context is the outer bound. To keep an idle connection
alive through home routers and NATs that drop silent TCP, the console
-writes a progress line at least every 15 s (§5.5).
+writes and flushes a progress line at least every 15 s (§5.5, codex 19).
### 5.3 The invariants
@@ -320,13 +325,48 @@ The envelope is unchanged from rev 3:
runs the query in a worker, and writes sealed chunks as the output
arrives. It checks for `cancel` every 5 s. When the worker ends, it
confirms every chunk and creates `result.json`.
-6. The console, inside the same request, reads `picked` until it appears,
- then lists `out/` and reads `result.json` about once a second. It writes
- to the response, in order: `{"picked": <box>}` once, `{"chunk": <seq>,
- "data": <base64 sealed bytes>}` for each new chunk, `{"wait": <phase>}`
- whenever 15 s pass with nothing else to send, and finally
- `{"result": <result.json>}`. Each line is written under a fresh write
- deadline, as `dbStream` does.
+6. The console, inside the same request, reads `picked` until it appears.
+ Then, about once a second, it reads `result.json` **first** and lists
+ `out/` **second**, the order the run's follow door uses for the same
+ reason (`internal/console/apirunsfollow.go:101` to `106`): a listing
+ taken before the result could miss a chunk the box confirmed just
+ before writing it **(codex 20)**. It writes to the response, in order:
+ `{"admitted": <id>}` at once, `{"picked": <box>}` once, `{"chunk":
+ <seq>, "data": <base64 sealed bytes>}` for each chunk in sequence order
+ with no gaps, `{"wait": <phase>}` whenever 15 s pass with nothing else
+ to send, and finally `{"result": <result.json>}`.
+ - **The result line comes last, and only when complete (codex 20).** It
+ is written only after every chunk from 1 to the result's open
+ `last_seq` has been written to the response. A result whose chunks are
+ not all listed yet is held, and the console keeps listing once a
+ second until they are, or until the request's own deadline ends it
+ with DBF-12. The console cannot open the sealed summary; the CLI still
+ checks the bytes against it (step 8).
+ - **Every line is flushed (codex 19).** Each line is written under a
+ fresh write deadline and then pushed out with
+ `http.ResponseController.Flush`, under the same deadline; a write or
+ flush error ends the request as a stalled client (step 7). Go buffers
+ small writes in the response writer, and `dbStream` flushes only at the
+ end, so without an explicit flush the wait lines would sit in the
+ console for minutes. The response's first line, `admitted`, is flushed
+ with the status and headers, so the headers leave at once.
+ - **Nothing downstream buffers it.** The response is
+ `Content-Type: application/x-ndjson` with `Cache-Control: no-store`
+ and no `Content-Length`. The edge's compressor does not gzip that
+ type (`compressibleType`, `internal/edge/compress.go:128`), and its
+ gzip writer passes `Flush` through when it does compress
+ (`compress.go:319`). The edge's reverse proxy flushes a response with
+ no `Content-Length` immediately (`httputil.ReverseProxy`), and
+ `no-store` keeps the cache recorder out.
+ - **Backhaul.** A request that reaches the console through another
+ region's edge travels over backhaul, whose transport waits at most 30 s
+ for response headers (`backhaulHeaderTimeout`,
+ `internal/edge/edge.go:2827`, used at `2958`). So the console bounds
+ everything before the `200` (the gate, the record read, the capability
+ read and the two creates) with a 20 s context, refusing with today's
+ "could not read the instance. Try again." on overrun, and flushes the
+ `200` and `admitted` the moment the creates land. After the headers,
+ backhaul sets no further timeout.
7. The console ends the response **without** a result line, and the CLI
says DBF-3 or DBF-12, when:
- no `picked` has appeared 2 minutes after `issued_at` (DBF-3);
@@ -436,9 +476,15 @@ the next midnight UTC, and the noncurrent version eligible a day after
that. S3 then removes eligible objects asynchronously, usually within a day
or two, with no hard deadline. So the honest statement is: **a read's
ciphertext lives until lifecycle expiry, typically two to four days after
-the read, with no guaranteed upper bound.** Per read it is at most the
-output cap (64 MiB of rows in phase 1). Plaintext rows are never in the
-bucket. A deployment that needs a hard deadline needs a different
+the read, with no guaranteed upper bound.** The size per read is the
+plaintext limit plus sealing overhead **(codex 7)**: the output cap limits
+the rows to 64 MiB of plaintext in phase 1; each chunk object stores its
+sealed envelope as raw bytes (not base64), which adds under 100 bytes per
+256 KiB chunk (a 65-byte sender key, a 12-byte nonce, a 16-byte tag and a
+short prefix), so a full answer stores about 64.03 MiB of ciphertext. The
+request, `picked`, `result.json`, the queue entry and `cancel` add a few
+kilobytes. Base64 appears only on the wire to the CLI. Plaintext rows are
+never in the bucket. A deployment that needs a hard deadline needs a different
reclamation mechanism (version-aware deletes), which this design does not
have.
@@ -455,10 +501,19 @@ privilege). It admits a new read, or a probe, only when the number of
active, activating or deactivating `carlos-dbread@` units is no more than
the number of workers it is itself running. Any other unit is an orphan
whose scratch is not in the table, so the agent refuses work until it is
-gone. There is no timeout escape: an orphan worker stops when its
-connection is gone (§8.3), and `RuntimeMaxSec` stops it in the end
+gone. There is no timeout escape: an orphan worker stops within 5 s of
+losing its connection (§8.3), and `RuntimeMaxSec` stops it in the end
regardless.
+**Restart reconciliation (codex 8).** A restarted agent's table is empty,
+and it cannot know the sizes of workers it did not start. So it reconciles
+by refusal, against the units actually running: at start, and before every
+admission, it lists `carlos-dbread@` units, and while any is active,
+activating or deactivating that it is not itself running, it takes no read
+and no probe. It never guesses a size for an orphan and never admits beside
+one. Its first admission therefore happens only after every unit from
+before the restart has stopped and systemd has removed its scratch.
+
### 5.8 Cost
Each capable box does one LIST of its region's current and previous hour of
@@ -699,9 +754,16 @@ header said and any write past the header's totals.
starts, the worker keeps one goroutine reading its connection. EOF or an
error there cancels the one context that restore, `lease.CheckDBContext`
(`internal/lease/check.go:44`, never `CheckDB`, which supplies a background
-context) and the query all run under (each stops on its context: `db.go`'s
-`runQueryContext`), and the worker exits. A
-worker blocked in a write to a dead connection gets an error at once. So an
+context) and the query all run under, and the worker exits. Not every
+step honours a context promptly: litestream's file replica client opens a
+plain file without checking it
+(`litestream@v0.5.14/file/replica_client.go:135`), and the LTX compactor's
+page reads and writes ignore it (`ltx@v0.5.1/compactor.go:177`, `199`). So
+the watcher does not rely on cancellation alone: after cancelling, it
+waits 5 s for the worker to finish, and if it has not, it ends the process
+itself with `os.Exit` **(codex 8)**. A process exit stops a compaction
+wherever it is. A worker blocked in a write to a dead connection gets an
+error at once. So an
agent restart ends every worker it had within seconds, systemd stops each
unit and removes its private `/var/tmp`, and only then does the restarted
agent's orphan count (§5.7) reach zero and admit work. While the agent
@@ -887,17 +949,18 @@ labelled so; they are not acceptance.
| T4 | Forgery and replay: unsigned, wrong key, scoped key, envelope moved to another id and to another region, a forged queue entry, the same envelope offered again before and after the agent restarts (`picked` holds, codex 5), a chunk sealed to another key, a chunk reordered, the last chunk dropped. And the boundary §5.6 states, as a documented negative: a writer that creates `picked`, chunks and a matching result first is accepted by the CLI (codex 18). | real verify and seal code, real store |
| T5 | Two capable boxes in one region: exactly one runs each read, across 1,000 reads with random jitter. A box whose `picked` create commits on the server but answers an error reads it back and runs the read; a box whose create answers an error when another box owns it steps away (codex 16). | real store conditional create, a store wrapper that loses answers |
| T6 | Parity: the forwarded query and the in-region query print byte-identical stdout for a fixture database (NULL, tabs, newlines, blobs, times). `--at` stops at the same transaction as the in-region door for the same replica and time, including a write in the same second as the target. | real restore, both paths |
-| T7 | The long request through the real path: a read that holds the request for 21 minutes passes through the real edge and the real console server (with its 5-minute `ReadTimeout`) and returns its result; the wait lines keep it alive; a CLI context of 23 minutes is the outer bound. | real edge, real console, real CLI binary |
+| T7 | The long request through the real path: a read that holds the request for 21 minutes passes through the real edge and the real console server (with its 5-minute `ReadTimeout`) and returns its result; a CLI context of 23 minutes is the outer bound. During a quiet restore the real CLI receives the `admitted` line within a second and a wait line at least every 15 s, both through the local edge and through backhaul from another edge, whose 30 s header timeout the request survives (codex 19). | real edges, real console, real CLI binary |
| T8 | Mixed versions: the five rows of §9.2, with the forwarding-off 409 compared byte for byte against main. | real console |
| T9 | Record changes between request and pick, during planning, during download and between download and `run`: moved, recreated in the same account, recreated in another account, class changed. Each ends with its code, the code reaches the CLI in the same response (codex 15), and no chunk is ever written (codex 1). | real records, a store wrapper that pauses the download |
| T10 | Approved copy digests for every row in §10, in the `approved_copy_test.go` pattern. | |
| T11 | CLI buffering: a dropped or bad last chunk, a summary hash mismatch, and a response that ends without a result each leave stdout empty (codex 6). | real CLI binary through `carlosTestEnv` |
-| T12 | Disk: two concurrent reads on one box whose free space fits one of them: one runs, the other ends `no-room`. An agent killed mid-read: the restarted agent takes no read and no probe while the old worker unit is active, its worker stops on EOF, and the restarted agent admits work only once the unit and its scratch are gone (codex 8). | real agent, real units, root runner |
+| T12 | Disk: two concurrent reads on one box whose free space fits one of them: one runs, the other ends `no-room`. An agent killed mid-read: the restarted agent takes no read and no probe while the old worker unit is active, its worker stops on EOF, and the restarted agent admits work only once the unit and its scratch are gone, without guessing a size for it. A disconnect during local compaction, after the download, still ends the worker within 5 s (codex 8). | real agent, real units, root runner |
| T13 | Readiness: a present socket with the service missing, masked, failing to start and speaking another protocol version each withdraws `db-read-1` within one probe and edges pass. A box running two reads keeps the capability, and its probe succeeds on the third connection (codex 9). | real units, root runner |
| T14 | Console evidence: status snapshots unreadable, one snapshot missing, an edges file unreadable, an empty edges listing beside a fresh status snapshot. Each admits the read; only complete evidence (both flags) gives DBF-1 or DBF-2 (codex 10). | real `ReadLiveSnapshot` over a real store |
| T15 | Admission (codex 17): two reads whose CLIs disconnect before pickup, and two whose CLIs disconnect after it, each free their person's and the console's count when the handler returns, without a console restart; a third read from the same person is then admitted. A console restart mid-read leaves nothing to recover. | real console |
| T16 | Late writes are harmless (codex 4, 13, 14): a box that keeps writing chunks and its result after the console's request has ended (by timeout and by disconnect) changes nothing any reader sees, and no protocol path issues a delete or a replacing write (a store wrapper fails the test on any `Delete` or non-create `Put` under `db-reads/`). | real console, real agent, a versioned MinIO bucket |
| T17 | The lifecycle rule: the module's rule covers `db-reads/` with current and noncurrent expiry and expired delete markers (a module test). | module test |
+| T18 | The result overtaking the last chunk: a store wrapper that lets the box commit its last chunk and `result.json` between the console's listing and its result read, in both orders. The console never writes the result line before every chunk through `last_seq`, and the read succeeds (codex 20). | real console, real store |
Mutation checks that must turn a test red: drop envelope verification (T4);
take the host or prefix from anywhere but the record and config (T1 with a
@@ -906,7 +969,9 @@ second tenant); remove `PrivateNetwork` (T3); skip the summary hash check
release the console's counts anywhere but the handler's return (T15); skip
the read-back of an ambiguous `picked` (T5); drop the orphan count (T12);
give probes no connection of their own (T13); remove the read-deadline
-extension (T7).
+extension or any flush (T7); list chunks before reading the result, or
+write the result line before its last chunk (T18); drop the worker's 5 s
+exit (T12).
## 12. Provider boundary
@@ -999,7 +1064,7 @@ named.
request, audit, the CLI's buffered forwarded path, DBF copy,
`docs/site/cli.md`, and the `deployment-buckets` lifecycle rule with the
approval plan for applying it. Gates: T1, T6, T7, T8, T10, T11, T14,
- T15, T16, T17, copy review, census overlay. Roll per §11.3.
+ T15, T16, T17, T18, copy review, census overlay. Roll per §11.3.
3. **`pull` and `pull --at`.** On provisional ruling 1, re-confirmed with
the storage statement in §16. Sealed LTX files in the same single
request, local restore and `CheckDBContext` in the CLI, and the pull copy
@@ -1110,3 +1175,10 @@ Finding 7's fix is the honest timing statement, not a new mechanism; a hard
bound would need version-aware deletes, which is noted for Paul in §16
rather than built. Finding 18 is fixed by withdrawing the claim and stating
the trust boundary (§5.6), not by adding per-box signing.
+
+**Round 4** (`CODEX-FINDINGS-DB-REGION-FORWARD-R4.md`). Every citation was
+checked and holds, including litestream's file client and the LTX
+compactor ignoring their contexts, and the backhaul transport's 30 s header
+timeout. Nothing is declined. All four fixes reuse what the spec already
+had: the run's result-before-list order, `http.ResponseController`, the
+orphan refusal, and a process exit.
diff --git a/docs/superpowers/specs/CODEX-REVIEW-BRIEF-DB-REGION-FORWARD.md b/docs/superpowers/specs/CODEX-REVIEW-BRIEF-DB-REGION-FORWARD.md
index e96d36109..5bbd95bd8 100644
--- a/docs/superpowers/specs/CODEX-REVIEW-BRIEF-DB-REGION-FORWARD.md
+++ b/docs/superpowers/specs/CODEX-REVIEW-BRIEF-DB-REGION-FORWARD.md
@@ -7,79 +7,33 @@ yet; you are reviewing the design against the code on `main`.
**Spec:** `docs/superpowers/specs/2026-10-22-db-region-forward-design.md`,
at the commit named in the prompt. Read `AGENTS.md` first.
-## Round 4
-
-This is round 4. Rev 4 of the spec answers your round 3 findings,
-`docs/superpowers/specs/CODEX-FINDINGS-DB-REGION-FORWARD-R3.md` (reviewed
-at `20524df4`). Rounds 1 and 2 are in `CODEX-FINDINGS-DB-REGION-FORWARD.md`
-and `-R2.md`. Each change is marked **(codex N)** where it lands, and §18
-records what was declined.
-
-Every round so far found defects from one source: a read was a durable,
-resumable object with its own lifecycle (follow, cancel, lazy expiry,
-console-written `lost` and `expired`, admission that outlived the
-request). Rev 4 removes that lifecycle. **A forwarded read is one
-synchronous HTTP request** (§5.2): the CLI's request stays open for the
-whole read, the console waits on the box's objects inside it and streams
-them back as newline-delimited JSON, and when the request ends the read is
-over for everyone. There is no follow route, no cancel route, no
-console-written `picked`, `lost` or `expired`, and the store's lifecycle
-rule is the only deletion. Rev 3's seven invariants are replaced by five,
-J1 to J5 (§5.3).
-
-| # | Finding | Round 3 said | Rev 4's answer |
+## Round 5
+
+This is round 5. Rev 5 of the spec answers your round 4 findings,
+`docs/superpowers/specs/CODEX-FINDINGS-DB-REGION-FORWARD-R4.md` (reviewed
+at `010d29f6`). Earlier rounds are in `CODEX-FINDINGS-DB-REGION-FORWARD.md`,
+`-R2.md` and `-R3.md`. Round 4 found the five invariants J1 to J5 (§5.3)
+fixed and the design converged. Rev 5 adds no mechanism; it makes four
+direct fixes, each marked **(codex N)**:
+
+| # | Finding | Round 4 said | Rev 5's answer |
|---|---|---|---|
-| 1 | incarnation check | fixed; pinning decline agreed | unchanged, §8.2 |
-| 2 | socket on stdin | fixed | unchanged, §8.3 |
-| 3 | progress versus completion ack | fixed | removed by the synchronous design |
-| 4 | late writes after `lost` | fixed | removed by the synchronous design: the console never writes `lost` |
-| 5 | cleanup reopens the election | fixed | J2, J4 |
-| 6 | "showed none" | fixed | unchanged; a response that ends without a result also prints nothing, §5.5 |
-| 7 | stored-ciphertext bound | partly | J4 with an honest timing statement and no hard bound, §5.7; ruling 1's bound flagged for Paul, §16 |
-| 8 | disk reservations | fixed | unchanged, and `CheckDBContext` named, §8.3 |
-| 9 | worker readiness | fixed | unchanged |
-| 10 | snapshot evidence | fixed | unchanged |
-| 11 | separate store endpoints | decline agreed | unchanged |
-| 12 | census | fixed | updated for one route and no delete site, §13 |
-| 13 | completion deletes terminal evidence | partly | removed by the synchronous design: nothing needs the evidence after the request ends |
-| 14 | cleanup removes follow identity | partly | removed by the synchronous design: there is no follow |
-| 15 | full db gate on follow routes | fixed | removed by the synchronous design: one route, one gate at the start, `record-changed` in the same response |
-| 16 | lazy expiry strands a read | new | removed by the synchronous design (the console writes no `picked` or result); J3's read-back for a box's ambiguous `picked`, §5.4 |
-| 17 | abandoned reads hold admission | new | J5: the console's counts are released by the handler's return, §5.7 |
-| 18 | the forgery claim contradicts sealing | new | withdrawn; §5.6 states the trust boundary plainly |
-
-The provisional rulings in §16 await Paul. Do not argue them; report only
-a mechanism that fails to honour one. Ruling 1's bound is flagged by the
-spec itself; you need not report it again.
-
-**Your answer has three parts, in this order.**
-
-1. **A disposition table** for findings 1 to 18: fixed, declined with an
- acceptable reason, or not fixed. Check each answer against the code, not
- against the spec's summary of it.
-2. **An invariant check.** For each of J1 to J5 in §5.3: does every
- mechanism in the spec keep it (name any write, route, timer or state
- that breaks it, with the section), and does the mapping table in §5.3
- hold, finding by finding? Look in particular for:
- - **the long request.** §5.2 claims that nothing on the real path (the
- edge's public listeners and reverse proxies, the console's
- `http.Server` with its 5-minute `ReadTimeout`, the CLI's clients)
- cuts a request of up to 22 minutes, and that the console can lift its
- read deadline per request as the import door does. Check each number
- and line it cites, and Go's actual `ReadTimeout` behaviour once a
- handler has read its body. Name any timeout the spec missed;
- - anything on the console side that outlives the request (J1), or an
- admission count released anywhere but the handler's return (J5);
- - any protocol delete or replacing write (J2, J4);
- - the box's `picked` read-back rule (J3) against a create whose answer is
- lost on S3 and on MinIO;
- - whether a box that keeps running after the console gave up can harm a
- later read, a later request with the same envelope, or the disk;
- - whether the storage statement in §5.7 is now true, without a hard
- bound it cannot keep.
-3. **Only new findings.** Do not repeat an earlier finding unless its fix
- is wrong or incomplete; then say so in the table and give the new
- evidence.
+| 7 | stored-ciphertext wording | partly, P3 | the 64 MiB plaintext limit and the stored ciphertext are now separate: raw sealed envelopes, under 100 bytes of overhead per 256 KiB chunk, base64 only on the wire, §5.7 |
+| 8 | restart recovery for disk reservations | partly, P2 | reconciliation by refusal against the units actually running, never a guessed size; and a worker that loses its connection exits by itself within 5 s, because litestream's file client and the LTX compactor ignore cancellation, §5.7, §8.3 |
+| 19 | progress writes not guaranteed to leave the console | new, P2 | every line flushed with `ResponseController.Flush` under its write deadline; `admitted` flushed with the headers; `application/x-ndjson` is not gzipped by the edge; work before the `200` bounded at 20 s for backhaul's 30 s header timeout, §5.5 step 6 |
+| 20 | the result can overtake the final chunk | new, P2 | result read before the listing, as the run's follow door does; the result line written only after every chunk through `last_seq`, §5.5 step 6 |
+
+Findings 1 to 6 and 9 to 18 were fixed or declined-agreed in round 4 and
+are unchanged.
+
+**Your answer has two parts, in this order.**
+
+1. **A disposition table** for findings 1 to 20: fixed, declined with an
+ acceptable reason, or not fixed. Check rev 5's four answers against the
+ code, not against the spec's summary of it.
+2. **Only new findings of severity P0, P1 or P2.** Leave out P3s this
+ round. Do not repeat an earlier finding unless its fix is wrong or
+ incomplete; then say so in the table and give the new evidence.
## Fixed rulings: do not re-argue them
@@ -220,6 +174,5 @@ A numbered list of findings. Each one gives:
Then a one-line verdict: accept, or revise. Do not rewrite the spec. No
praise.
-On every round after the first, follow the three-part answer in the round
-section at the top: the disposition table, the invariant check, then only
-new findings.
+On every round after the first, follow the answer format in the round
+section at the top.