carlos / platform Public

edge asset cache plans: Astra plan review round 3 folded in (plans 1 and 3)

Summary

Round 3 fixes for plans 1 and 3: failed retirements make an app incomplete, ship backfill runs on the re-ship path, cold HEAD is buffered, billing follows the delivery on a late veto, the provider test uses the full import path, and the production refusal matrix is complete.
claude agent, operated by paul@keymail.dev db41b9ba5ce71a3f358a5ee8db7e66744adaffe3 parent 1a31b90
2 files changed, +12 −9
  • docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md +5 −4
  • docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md +7 −5

CI passed — run details

CI log
=== gate ===
.amadan/test-gate.sh
=== launchctx (1 s)
ok  	amadan.net/carlos/platform.git/launchctx	0.012s
?   	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 (156 s)
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.972s
ok  	github.com/carlosframework/platform/cmd/moveprobe	11.582s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.707s
ok  	github.com/carlosframework/platform/internal/activator	56.261s
ok  	github.com/carlosframework/platform/internal/agentsock	0.017s
ok  	github.com/carlosframework/platform/internal/alerting	0.017s
ok  	github.com/carlosframework/platform/internal/analytics	0.022s
ok  	github.com/carlosframework/platform/internal/appconfig	2.865s
ok  	github.com/carlosframework/platform/internal/apperr	0.085s
ok  	github.com/carlosframework/platform/internal/applog	0.135s
ok  	github.com/carlosframework/platform/internal/awsinv	0.017s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.017s
ok  	github.com/carlosframework/platform/internal/boxid	1.000s
?   	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.017s
ok  	github.com/carlosframework/platform/internal/caps	0.017s
ok  	github.com/carlosframework/platform/internal/certwarm	0.017s
ok  	github.com/carlosframework/platform/internal/cgroup	0.709s
ok  	github.com/carlosframework/platform/internal/changelog	0.359s
ok  	github.com/carlosframework/platform/internal/claimactivation	1.005s
ok  	github.com/carlosframework/platform/internal/clamd	0.060s
?   	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	8.393s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.026s
ok  	github.com/carlosframework/platform/internal/dbquery	0.017s
ok  	github.com/carlosframework/platform/internal/dbreads	2.108s
?   	github.com/carlosframework/platform/internal/dbreads/dbreadstest	[no test files]
ok  	github.com/carlosframework/platform/internal/dbverify	0.020s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.019s
ok  	github.com/carlosframework/platform/internal/docsite	0.006s
ok  	github.com/carlosframework/platform/internal/domains	2.890s
ok  	github.com/carlosframework/platform/internal/doorkey	0.006s
ok  	github.com/carlosframework/platform/internal/economics	0.005s
ok  	github.com/carlosframework/platform/internal/edge	45.404s
?   	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.133s
ok  	github.com/carlosframework/platform/internal/gate	1.791s
ok  	github.com/carlosframework/platform/internal/genselect	0.015s
ok  	github.com/carlosframework/platform/internal/geo	0.020s
ok  	github.com/carlosframework/platform/internal/geoip	0.030s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.159s
ok  	github.com/carlosframework/platform/internal/host	145.392s
ok  	github.com/carlosframework/platform/internal/hostactors	6.694s
ok  	github.com/carlosframework/platform/internal/hostactors/awsmembership	0.006s
ok  	github.com/carlosframework/platform/internal/hostbox	3.284s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.928s
ok  	github.com/carlosframework/platform/internal/hostenv	0.009s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.009s
ok  	github.com/carlosframework/platform/internal/hostexec	5.626s
ok  	github.com/carlosframework/platform/internal/hostfence	0.033s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.287s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.048s
ok  	github.com/carlosframework/platform/internal/hostguard	1.138s
ok  	github.com/carlosframework/platform/internal/hostops	2.232s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.003s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.005s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.817s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.237s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.001s
ok  	github.com/carlosframework/platform/internal/infraview	0.002s
ok  	github.com/carlosframework/platform/internal/instancectl	5.257s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.850s
ok  	github.com/carlosframework/platform/internal/instancefs	0.011s
ok  	github.com/carlosframework/platform/internal/instancekey	0.004s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.009s
ok  	github.com/carlosframework/platform/internal/layout	1.945s
ok  	github.com/carlosframework/platform/internal/lbhealth	0.007s
ok  	github.com/carlosframework/platform/internal/lease	10.158s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.654s
ok  	github.com/carlosframework/platform/internal/ledger	0.026s
ok  	github.com/carlosframework/platform/internal/mail	0.248s
ok  	github.com/carlosframework/platform/internal/maintenance	0.077s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.559s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.077s
ok  	github.com/carlosframework/platform/internal/metrics	0.013s
ok  	github.com/carlosframework/platform/internal/moves	21.449s
ok  	github.com/carlosframework/platform/internal/moves/archive	1.103s
ok  	github.com/carlosframework/platform/internal/objstore	0.270s
ok  	github.com/carlosframework/platform/internal/ops	0.192s
ok  	github.com/carlosframework/platform/internal/park	0.318s
ok  	github.com/carlosframework/platform/internal/plans	0.006s
ok  	github.com/carlosframework/platform/internal/privatefile	0.004s
ok  	github.com/carlosframework/platform/internal/provision	15.443s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.015s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	5.733s
ok  	github.com/carlosframework/platform/internal/relay	7.198s
ok  	github.com/carlosframework/platform/internal/release	25.945s
ok  	github.com/carlosframework/platform/internal/rollout	3.889s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.006s
ok  	github.com/carlosframework/platform/internal/rootaws	0.009s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.061s
ok  	github.com/carlosframework/platform/internal/rootstate	0.029s
ok  	github.com/carlosframework/platform/internal/runs	0.516s
ok  	github.com/carlosframework/platform/internal/s3compat	0.006s
ok  	github.com/carlosframework/platform/internal/safefile	0.005s
ok  	github.com/carlosframework/platform/internal/schedule	2.689s
ok  	github.com/carlosframework/platform/internal/seeds	0.011s
ok  	github.com/carlosframework/platform/internal/sim/engines	2.696s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	10.670s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.884s
ok  	github.com/carlosframework/platform/internal/status	0.086s
ok  	github.com/carlosframework/platform/internal/steering	0.007s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.246s
ok  	github.com/carlosframework/platform/internal/svcrole	0.005s
ok  	github.com/carlosframework/platform/internal/telemetry	0.006s
ok  	github.com/carlosframework/platform/internal/termfence	0.004s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.006s
ok  	github.com/carlosframework/platform/internal/unixdial	0.003s
ok  	github.com/carlosframework/platform/internal/updatewords	0.004s
ok  	github.com/carlosframework/platform/internal/usage	0.005s
ok  	github.com/carlosframework/platform/internal/version	0.001s
ok  	github.com/carlosframework/platform/internal/vet	1.652s
=== internal_console@0 (80 s)
ok  	github.com/carlosframework/platform/internal/console	77.680s
=== internal_console@1 (45 s)
ok  	github.com/carlosframework/platform/internal/console	42.383s
=== internal_console@2 (41 s)
ok  	github.com/carlosframework/platform/internal/console	38.261s
=== internal_console@3 (41 s)
ok  	github.com/carlosframework/platform/internal/console	38.519s
=== internal_console@4 (43 s)
ok  	github.com/carlosframework/platform/internal/console	40.615s
=== internal_console@5 (42 s)
ok  	github.com/carlosframework/platform/internal/console	39.557s
=== internal_console@6 (37 s)
ok  	github.com/carlosframework/platform/internal/console	34.475s
=== internal_console@7 (47 s)
ok  	github.com/carlosframework/platform/internal/console	44.275s
=== root@0 (109 s)
ok  	github.com/carlosframework/platform	107.271s
=== root@1 (84 s)
ok  	github.com/carlosframework/platform	82.713s
=== root@2 (91 s)
ok  	github.com/carlosframework/platform	89.095s
=== 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: 50528418 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.02s)
    --- 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.01s)
        --- 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.024s
=== RUN   TestT4StorageProbeMinio
2026/10/24 19:10:54 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18E18CE7DDF3C6EA, 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.021s
=== RUN   TestS3StoreMinioContract
=== RUN   TestS3StoreMinioContract/conditional_get_and_etag_listing
=== RUN   TestS3StoreMinioContract/bump_through_every_mutation_path
--- PASS: TestS3StoreMinioContract (0.03s)
    --- PASS: TestS3StoreMinioContract/conditional_get_and_etag_listing (0.01s)
    --- PASS: TestS3StoreMinioContract/bump_through_every_mutation_path (0.02s)
PASS
ok  	github.com/carlosframework/platform/internal/release	0.030s
Bucket created successfully `local/dbf-deploy`.
Bucket created successfully `local/dbf-data-ie`.
Bucket created successfully `local/dbf-data-uk`.
local/dbf-deploy versioning is enabled
Created policy `dbf-console` successfully.
Created policy `dbf-ukbox` successfully.
Added user `dbfconsole` successfully.
Added user `dbfukbox` successfully.
Attached Policies: [dbf-console]
To User: dbfconsole
Attached Policies: [dbf-ukbox]
To User: dbfukbox
=== RUN   TestDBForwardMinio
2026/10/24 19:10:56 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000000 txid.db=0000000000000001
2026/10/24 19:10:56 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000001 maxTXID=0000000000000001 size=720
2026/10/24 19:10:56 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000001 txid.db=0000000000000001
2026/10/24 19:10:56 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000001 txid.db=0000000000000002
2026/10/24 19:10:56 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000002 maxTXID=0000000000000002 size=370
2026/10/24 19:10:56 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000002 txid.db=0000000000000002
2026/10/24 19:10:56 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000002 txid.db=0000000000000003
2026/10/24 19:10:56 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000003 maxTXID=0000000000000003 size=454
2026/10/24 19:10:56 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000003 txid.db=0000000000000003
2026/10/24 19:10:56 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000000 txid.db=0000000000000001
2026/10/24 19:10:56 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000001 maxTXID=0000000000000001 size=720
2026/10/24 19:10:56 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000001 txid.db=0000000000000001
2026/10/24 19:10:56 console: CARLOS_SECRETS_PUBKEY is unset, so no region outside the home can be chosen automatically
2026/10/24 19:10:56 console: could not notify member@example.com of a CLI token issue: keymail: lookup member@example.com: keymail: GET /api/federation/lookup?addr=member%40example.com&from=console%40127.0.0.1%3A44213: could not determine target mailbox
    agent.go:135: dbreads: dbr_cj2xtfhknozdxcrnqusqvmesua: planned 3 files (1544 bytes) and restored to 3 in 10ms
    agent.go:135: dbreads: dbr_cj2xtfhknozdxcrnqusqvmesua: output 5 bytes in 1 confirmed chunks in 1ms
2026/10/24 19:10:57 console: db read dbr_cj2xtfhknozdxcrnqusqvmesua: relayed 1 chunks (102 bytes) in 1.004s, ended done
    agent.go:135: dbreads: dbr_e7og2t2l2sqpk4konrh7asf62i: planned 1 files (720 bytes) and restored to 1 in 8ms
    agent.go:135: dbreads: dbr_e7og2t2l2sqpk4konrh7asf62i: output 5 bytes in 1 confirmed chunks in 1ms
2026/10/24 19:10:58 console: db read dbr_e7og2t2l2sqpk4konrh7asf62i: relayed 1 chunks (102 bytes) in 1.003s, ended done
    agent.go:135: dbreads: dbr_hjk6repgmrq7cqdmjwvghs3gma: planned 3 files (1544 bytes) and held through 3 in 8ms
    agent.go:135: dbreads: dbr_hjk6repgmrq7cqdmjwvghs3gma: output 1544 bytes in 1 confirmed chunks in 1ms
2026/10/24 19:10:59 console: db read dbr_hjk6repgmrq7cqdmjwvghs3gma: relayed 1 chunks (1641 bytes) in 1.004s, ended done
=== RUN   TestDBForwardMinio/by_disconnect
=== NAME  TestDBForwardMinio
    agent.go:135: dbreads: dbr_px5arui5zyz3nha2eqeineehgy: planned 3 files (1544 bytes) and restored to 3 in 10ms
2026/10/24 19:10:59 console: db read dbr_px5arui5zyz3nha2eqeineehgy: relayed 0 chunks (0 bytes) in 32ms, ended client gone
    agent.go:135: dbreads: dbr_px5arui5zyz3nha2eqeineehgy: output 5 bytes in 1 confirmed chunks in 21ms
=== RUN   TestDBForwardMinio/by_timeout
=== NAME  TestDBForwardMinio
    agent.go:135: dbreads: dbr_hch46iutetyaaunhgsre4z4aj4: planned 3 files (1544 bytes) and restored to 3 in 9ms
2026/10/24 19:11:19 console: db read dbr_hch46iutetyaaunhgsre4z4aj4: relayed 0 chunks (0 bytes) in 19.212s, ended no-result
    agent.go:135: dbreads: dbr_hch46iutetyaaunhgsre4z4aj4: output 5 bytes in 1 confirmed chunks in 19.189s
--- PASS: TestDBForwardMinio (22.59s)
    --- PASS: TestDBForwardMinio/by_disconnect (0.16s)
    --- PASS: TestDBForwardMinio/by_timeout (19.32s)
PASS
ok  	github.com/carlosframework/platform/internal/console	22.607s
minio contract: PASS
diff --git a/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md b/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md
index 0db4145e9..e8d39caf6 100644
--- a/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md
+++ b/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md
@@ -199,7 +199,7 @@ Task 0 drafts every row as `pending` and runs the copy review. Rows (draft text
- PATCH body field `Assets *string \`json:"assets"\`` taking `"on"` or `"off"` (stored as `Record.Assets`: nil for on, false for off)
- `POST /api/cli/apps/{account}/{app}/assets/purge`, role `Deploy` and `Publish` and `Admin`; reply `{"instances":[{"host":..., "gen":..., "ok":bool, "error":...}], "complete":bool}`
- `GET /api/cli/apps/{account}/{app}/assets`, any app role; reply per instance `{"host", "paths", "assets", "gen", "published_gen", "published_at"}`
- - `func mintMissingAssetGens(ctx context.Context, s release.Store, acct, app string) error` (ship door, after a manifest with `Assets` is stored)
+ - `func mintMissingAssetGens(ctx context.Context, s release.Store, acct, app string) error` (ship door): called before **both** successful ship responses whenever the manifest that will be served declares assets, the newly written one or, on the re-ship short-circuit (`apicli.go:2202`), the stored one, so a partly failed backfill is retried by shipping the same version again (astra P3-6); a failure is a 502 naming the instances not yet minted, after the manifest is stored
- [ ] **Step 1: Write the failing tests.**
- `TestInstanceCreateMintsAssetGen` and one per creator (seed, import, attach, replica, prepare): the new record has a 16-hex `AssetGen`.
@@ -207,9 +207,10 @@ Task 0 drafts every row as `pending` and runs the copy review. Rows (draft text
- `TestPurgeDrawsAFreshGenPerInstance` (three instances; every `AssetGen` changes and they differ from each other).
- `TestPurgeReportsPartialAndRetryCompletes` (a store wrapper failing the second record's write: reply `complete:false` naming it; a second call completes).
- `TestShipWithAssetsMintsMissingGenerations` (records without `AssetGen` get one; records with one keep it).
+ - `TestReshipRetriesAPartlyFailedBackfill` (the first ship's backfill fails for one record; shipping the same version again mints it).
- `TestAssetsStatusJoinsPublishedGeneration` (an edges file under `control/edges/` with an `assets` block is joined by host; uses `readEdgesFiles`).
- Route census and invariant fixtures for both new routes; `TestEveryCLIDoorNamesItsRoles` passes.
-- [ ] **Step 2:** Run `go test ./internal/console/ -run 'TestInstanceCreateMintsAssetGen|TestAPIInstanceUpdateAssets|TestPurge|TestShipWithAssetsMints|TestAssetsStatus|TestClaimConsoleRouteCensus|TestEveryCLIDoorNamesItsRoles' -count=1` and `go test ./internal/provision/ -run 'TestSetRecordAssetGen' -count=1`. Expected: FAIL.
+- [ ] **Step 2:** Run `go test ./internal/console/ -run 'TestInstanceCreateMintsAssetGen|TestAPIInstanceUpdateAssets|TestPurge|TestShipWithAssetsMints|TestReshipRetries|TestAssetsStatus|TestClaimConsoleRouteCensus|TestEveryCLIDoorNamesItsRoles' -count=1` and `go test ./internal/provision/ -run 'TestSetRecordAssetGen' -count=1`. Expected: FAIL.
- [ ] **Step 3:** Implement. The purge walks `provision.RecordsPrefix` like `placeAppTarget` (`fleets.go:578`) but continues past a failed write, reports each, and audits once with the list. Each door bumps the serial after writing. Add EAC-15 to `assetCacheCopyBuilt`.
- [ ] **Step 4:** Run both commands. Expected: PASS.
- [ ] **Step 5:** Commit.
@@ -225,9 +226,9 @@ Task 0 drafts every row as `pending` and runs the copy review. Rows (draft text
- `type ReconcileReport struct { Complete map[string]bool }` (keys `"<acct>/<app>"`)
- `ReconcileConfig.Report *ReconcileReport` (nil is today's behaviour; when set, `Reconcile` fills it before returning)
-An app is complete when: the listing was not `partial`; no record body under the app failed to read; no record under it was `protect`ed; and **every** in-scope record was positively applied, meaning every convergence step the pass runs for it (`convergeOwned`, the gate convergence at `reconcile.go:1366-1374`, a mint or an adoption) ran and returned nil, including steps that found nothing to change. There is no equality shortcut on a subset of columns: a record whose gate convergence failed is not applied even if its asset columns already match (astra P2-R1-7). A record the pass skipped for any other reason (a trash marker that could not be read sets `declineMint`, `reconcile.go:1137`, and the loop skips the record at `:1174` with no failing write; a held claim; a deferred adoption) makes its app incomplete (astra P1-7). `readFailed` from an unreadable account manifest makes every app of that account incomplete. A pass that returns an error leaves `Complete` empty.
+An app is complete when: the listing was not `partial`; no record body under the app failed to read; no record under it was `protect`ed; and **every** in-scope record was positively applied, meaning every convergence step the pass runs for it (`convergeOwned`, the gate convergence at `reconcile.go:1366-1374`, a mint or an adoption) ran and returned nil, including steps that found nothing to change. There is no equality shortcut on a subset of columns: a record whose gate convergence failed is not applied even if its asset columns already match (astra P2-R1-7). Retirements count too: when the pass decides to remove a retired route of the app and `claimRetire` fails (`reconcile.go:1597-1599` logs and continues), the app is incomplete, so a deleted hostname whose row survives cannot keep a renewed `at` through a sibling's healthy convergence (astra P3-3). A record the pass skipped for any other reason (a trash marker that could not be read sets `declineMint`, `reconcile.go:1137`, and the loop skips the record at `:1174` with no failing write; a held claim; a deferred adoption) makes its app incomplete (astra P1-7). `readFailed` from an unreadable account manifest makes every app of that account incomplete. A pass that returns an error leaves `Complete` empty.
-- [ ] **Step 1: Write the failing tests** (`setup`, `acmeRecord`, `putReplace` helpers): `TestReportCompleteOnACleanPass`; `TestReportOmitsAnAppWithAnUnreadableRecord` (a `viewCountingStore`-style wrapper failing one Get; the other app is still complete); `TestReportOmitsAProtectedRecordsApp` (malformed body); `TestReportOmitsEveryAppOnAPartialListing`; `TestReportEmptyWhenThePassFails`; `TestReportOmitsAnAppWhoseRecordWasSkipped` (an unreadable trash marker with the record's `AssetGen` changed: the row keeps the old generation and the app is not complete); `TestReportOmitsAnAppWhoseGateConvergenceFailed` (asset columns already equal, `SetGate` failing through a SQLite trigger as in Task 7); `TestReportNilKeepsTodaysBehaviour`.
+- [ ] **Step 1: Write the failing tests** (`setup`, `acmeRecord`, `putReplace` helpers): `TestReportCompleteOnACleanPass`; `TestReportOmitsAnAppWithAnUnreadableRecord` (a `viewCountingStore`-style wrapper failing one Get; the other app is still complete); `TestReportOmitsAProtectedRecordsApp` (malformed body); `TestReportOmitsEveryAppOnAPartialListing`; `TestReportEmptyWhenThePassFails`; `TestReportOmitsAnAppWhoseRecordWasSkipped` (an unreadable trash marker with the record's `AssetGen` changed: the row keeps the old generation and the app is not complete); `TestReportOmitsAnAppWhoseGateConvergenceFailed` (asset columns already equal, `SetGate` failing through a SQLite trigger as in Task 7); `TestReportOmitsAnAppWhoseRetirementFailed` (two instances; one record deleted and its route's delete failing through a trigger; the sibling converges; the app is not complete); `TestReportNilKeepsTodaysBehaviour`.
- [ ] **Step 2:** Run `go test ./internal/provision/ -run 'TestReport' -count=1`. Expected: FAIL.
- [ ] **Step 3:** Implement by tracking per-app failure inside the existing scan and convergence loops; no new reads.
- [ ] **Step 4:** Run the same command. Expected: PASS.
diff --git a/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md b/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md
index a296dae44..033e9b0e9 100644
--- a/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md
+++ b/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md
@@ -157,7 +157,7 @@ Added to `docs/superpowers/plans/2026-10-24-edge-asset-cache-copy.md` by Task 0
- `func (e *Edge) serveAssetFillForHop(w http.ResponseWriter, r *http.Request, host string, up upstream) bool` (true when it handled a verified, valid fill; it marks the request so the meter defer skips it)
- `func (e *Edge) fillFromApp(ctx context.Context, w http.ResponseWriter, r *http.Request, up upstream, k assetcache.Key, a HostAssets, onHeader func(v assetVerdict, h http.Header)) (*assetcache.Entry, error)`: builds the allow-listed request (`Host`, `User-Agent`, `Accept`, `Accept-Encoding: identity`; method `GET` even for `HEAD`), sends it through the activator for a hibernating route or `up.proxy` otherwise, reads the answering build from the response's `X-Carlos-Version` and asks `e.assets.local.Declared(host, build, r.URL.Path)` with the request hostname (astra P1-2), calls `assetAdmit`, calls `onHeader` before the first body byte, writes the body to a `Fill` while the verdict says store while hashing it with SHA-256, abandons the fill at the 60 s deadline, on a short body or on `assetTrailerVeto`, and on success sets a strong `ETag` of `"<hex sha256>"` in the stored header when the app sent none (spec §5.4; the far edge computes the same over the same bytes, so home and far validators match) and commits (astra P1-14).
-**The initiating request.** When the initiator's request has no `Range`, `If-None-Match`, `If-Modified-Since` or `If-Range`, the body is teed to it as it fills. When it has any of them, nothing is teed: the initiator waits for the commit and is answered by `serveAssetEntry`, so its conditional or range is honoured on a cold miss as on a hit (astra P1-10). If the verdict refuses at header time, the full response is streamed to it unmodified (a 200 to a range request is valid HTTP) with `Carlos-Cache: bypass`. If the fill is abandoned after the headers (deadline, short body, trailer veto), what happens depends on whether the initiator's response has started: a **streamed** initiator's delivery simply continues to the end of the upstream body exactly as an uncached response would, and only the caching is abandoned; nothing is ever replayed onto a started response. A **buffered** initiator (one with `Range` or a conditional, which nothing has been written to yet) has its request forwarded again as today and answered `bypass` (astra P2-1).
+**The initiating request.** When the initiator is a `GET` with no `Range`, `If-None-Match`, `If-Modified-Since` or `If-Range`, the body is teed to it as it fills. When it is a `HEAD`, or has any of those headers, it is **buffered** (astra P3-5): nothing is teed, the initiator waits for the commit and is answered by `serveAssetEntry`, so its conditional or range is honoured on a cold miss as on a hit (astra P1-10). If the verdict refuses at header time, the full response is streamed to it unmodified (a 200 to a range request is valid HTTP) with `Carlos-Cache: bypass`; a `HEAD` gets the headers and no body. If the fill is abandoned after the headers (deadline, short body, trailer veto), what happens depends on whether the initiator's response has started: a **streamed** initiator's delivery simply continues to the end of the upstream body exactly as an uncached response would, and only the caching is abandoned; nothing is ever replayed onto a started response. A **buffered** initiator (a `HEAD`, or one with `Range` or a conditional, which nothing has been written to yet) has its request forwarded again as today and answered `bypass` (astra P2-1). **Billing follows the delivery** (astra P3-4): the discarded attempt is metered by no one (the home did not meter an acknowledged fill, and the far edge meters an acknowledged response only when it is the one delivered), and the re-forwarded request is metered by whoever meters it today, so the client's one delivery is one request with only the delivered bytes.
On the home, `serveAssetFillForHop` runs only for a verified hop carrying `X-Carlos-Asset-Fill`; it checks `verifyAssetFill`, that `e.assets.local.For(host)` gives the asserted account, app and gen and catches the path; on any failure it returns false and the request continues as an ordinary hop. It answers a hit from the store with `X-Carlos-Asset-Expires` set to the entry's expiry, or fills; in both cases it deletes any app-set `X-Carlos-Asset-Fill-Ack` and `X-Carlos-Asset-Expires`, sets `X-Carlos-Asset-Expires` (`0` when the verdict does not store) and `X-Carlos-Asset-Fill-Ack`, and never touches the shared LRU.
@@ -172,13 +172,14 @@ On the home, `serveAssetFillForHop` runs only for a verified hop carrying `X-Car
- `TestFallbackETagIsTheBodyHash` (no app `ETag`: the entry's is `"<sha256>"`, a conditional hit with it is 304).
- `TestSetCookieTrailerVetoesTheFill` (announced and unannounced).
- `TestStreamedInitiatorIsNotReplayedAfterATrailerVeto` and `TestStreamedInitiatorIsNotReplayedAfterTheDeadline` (the client receives exactly one status line and one body, byte-identical to the upstream's).
+ - `TestColdHeadIsBuffered` (a cold `HEAD`: answered from the entry after commit with the fallback `ETag`; on refusal, headers and no body).
- `TestStalledFillIsAbandonedAtTheDeadline` (deadline hook `assetFillDeadline`; reservation released, `Stats().Reserved == 0`, waiters forwarded).
- `TestNonCanonicalPathBypasses`, `TestPostUnderAPrefixPassesThrough`.
- `TestHomeFillAcknowledgesAndSkipsTheMeter` (a verified hop with a valid assertion: response has a verifiable ack, the usage meter recorded nothing, the shared LRU has no entry).
- `TestHomeFillWithStaleGenGetsNoAck` and `TestHomeFillWithChangedOwnerGetsNoAck` (metered as an ordinary hop).
- `TestHomeFillUncacheableIsAcknowledgedWithZeroExpiry`.
- `TestAppSetAckHeadersAreStripped` (the backend sets both headers; the home's own replace them; on a plain request neither reaches the client).
-- [ ] **Step 2:** Run `go test ./internal/edge/ -run 'TestLocal|TestHibernatingRouteHit|TestHeadFills|TestQueryIsPart|TestFillSurvives|TestShortBody|TestStalledFill|TestNonCanonicalPath|TestPostUnder|TestHomeFill|TestAppSetAck|TestColdMiss|TestFallbackETag|TestSetCookieTrailer|TestStreamedInitiator' -count=1`. Expected: FAIL.
+- [ ] **Step 2:** Run `go test ./internal/edge/ -run 'TestLocal|TestHibernatingRouteHit|TestHeadFills|TestQueryIsPart|TestFillSurvives|TestShortBody|TestStalledFill|TestNonCanonicalPath|TestPostUnder|TestHomeFill|TestAppSetAck|TestColdMiss|TestColdHead|TestFallbackETag|TestSetCookieTrailer|TestStreamedInitiator' -count=1`. Expected: FAIL.
- [ ] **Step 3:** Implement.
- [ ] **Step 4:** Run the same command. Expected: PASS.
- [ ] **Step 5:** Commit.
@@ -202,7 +203,7 @@ On the home, `serveAssetFillForHop` runs only for a verified hop carrying `X-Car
- `TestFarFillCarriesNoCookie` (asserted at the fake home).
- `TestClientSentFillHeaderIsStripped`.
- `TestFarUpgradeUnderAPrefixIsBackhauledIntact` (a WebSocket `GET` under `/assets/`: not caught, upgrade headers reach the fake home).
- - `TestFarColdMissAnswersARangeFromTheEntry`.
+ - `TestFarColdMissAnswersARangeFromTheEntry` and `TestFarColdHeadIsBuffered`.
- `TestFarFallbackETagMatchesTheHomes` (no app `ETag`: home and far entries carry the same value).
- `TestFarCoalescedWaitersGetTheEntry` and `TestFarRefusedFillReachesOnlyTheInitiator` (differing `Accept`, a `Set-Cookie` response, an unacknowledged fill; each waiter's request reaches the fake home on its own).
- [ ] **Step 2:** Run `go test ./internal/edge/ -run 'TestFar|TestClientSentFillHeader' -count=1` (every Task 6 test name starts with one of these). Expected: FAIL.
@@ -253,7 +254,8 @@ The authority table itself runs in plan 1 Task 8 (`internal/host`, the real `ass
- `TestAssetE2EOldFarEdgeForgedMarkerIsBilledAsAHop`: an old far edge is simulated explicitly, because a new edge strips the client's marker whether or not it is armed: the test sends the home a hop it builds itself, with a valid `X-Carlos-Hop-Auth` from `signHop` under the scene's mesh key and a forged `X-Carlos-Asset-Fill` (a well-formed timestamp with a random MAC), which is exactly what an old edge forwarding a client header would send. The home's backend observes that the hop verified and the marker arrived; the home bills it as an ordinary hop (`TestHopServedRequestMetersOnceWithHopCounters`'s counters) and the response carries no acknowledgement (astra P2-5).
- `TestAssetE2EDisarmedHomeFillsNothing`: the home unarmed with a stale published entry still on the far edge: responses arrive `bypass`, metered once at the home, nothing stored.
- `TestAssetE2EFillCarriesNoCredentials`: a client `Cookie` and `Authorization` sent to the far edge never reach the home's backend.
- - `TestAssetE2ERefusedFillsAreStoredNowhere`: `Set-Cookie`, a `Set-Cookie` trailer, `Vary: Origin`, a long-lived HTML fallback and a 404, each through the far edge: answered `bypass`, both stores empty, metered once.
+ - `TestAssetE2ERefusedFillsAreStoredNowhere`: `Set-Cookie`, a `Set-Cookie` trailer, `Vary: Cookie`, `Vary: Origin`, `Cache-Control: private`, `no-cache`, a 206, a long-lived HTML fallback and a 404, each through the far edge: answered `bypass`, both stores empty, metered once (astra P3-R1-15).
+ - `TestAssetE2ELateVetoOnABufferedRangeBillsOnce`: a cold range request through the far edge whose acknowledged fill gets a `Set-Cookie` trailer: the client gets one response from the re-forward, and the two edges' meters together record one request and only the delivered bytes.
- `TestAssetE2ECoalescedWaitersAndRefusals`: concurrent far requests for one key share an admitted entry; with a refused response, each waiter reaches the home on its own.
- `TestAssetE2EStalledBodyIsAbandoned`: the home's backend stalls after headers; the far fill is abandoned at the deadline with both reservations released.
- `TestAssetE2EUndeclaringBuildIsNotStored`: during a generation switch where only the installed build declares the prefix, the serving build's cacheable fallback is stored on neither edge.
@@ -267,7 +269,7 @@ The authority table itself runs in plan 1 Task 8 (`internal/host`, the real `ass
- [ ] **Step 1:** Write EAC-23 and EAC-24 into `docs/site/cli.md`; add them to `assetCacheCopyBuilt`; `go test . -run 'TestAssetCacheCopy' -count=1` and `go test ./internal/docsite/ -count=1` PASS.
- [ ] **Step 2:** `git fetch origin && git merge origin/main`; census test FAILs naming the units and envelopes above; write the overlay and notes file; post on #280; census PASS.
-- [ ] **Step 3:** Provider boundary (spec §9): `TestAssetCodeImportsNoAWSSDK` in `internal/edge` runs `go list -deps ./internal/assetcache` and checks no `github.com/aws/` package appears, and parses the imports of this plan's `internal/edge/asset*.go` files with `go/parser` and checks the same. It does not check `internal/edge` as a whole, which imports the AWS SDK for its Route 53 adapter (`route53dns.go:25`) (astra P1-4). Task 9's e2e is the no-AWS-configuration run.
+- [ ] **Step 3:** Provider boundary (spec §9): `TestAssetCodeImportsNoAWSSDK` in `internal/edge` runs `go list -deps github.com/carlosframework/platform/internal/assetcache` (the full import path, since the test runs in the package directory) and checks no `github.com/aws/` package appears, and parses the imports of `asset*.go` in its own directory (package-local names, excluding `_test.go`) with `go/parser` and checks the same (astra P3-2). It does not check `internal/edge` as a whole, which imports the AWS SDK for its Route 53 adapter (`route53dns.go:25`) (astra P1-4). Task 9's e2e is the no-AWS-configuration run.
- [ ] **Step 4:** Cross-compiles and `gofmt -l .` clean; weights for any new test over a second; commit, push, CI green on the tip, merge per plan 1's Global Constraints.
### Task 11: Acceptance on dev (needs Paul's go-ahead to roll)