The final D-255 chain ticket: the Tier 2/3 disk-backed cache store per D-255(d) and the round-2 three-tier spec. step_canvas_disk_cache.gd stores the WIRE payload pre-decode (store_var/get_var round-trips the PNG PackedByteArrays natively — Image decode never runs in this file), keyed by the same composite key as Tier 1, with a per-body index.json and malformed-index recovery (rebuild-or-discard, never crash).
Eviction — three independent mechanisms, exactly as ruled
Rung-0 Global: retention floor, no sweep ever (also threaded into Tier 1 per the T-1182 handoff).
Tier 2 geometry: byte-valid forever — evicted only by time-since-last-visit (14d starting tunable, on body-open) and LRU byte budget (256 MiB/body, 5-min coarse timer), two separate sweeps per the ruling.
Tier 3 sim-state TTL: wired and tested; no production caller yet (no sim-state field exists on EncodedStepCanvas — the documented D-253 stub inheritance).
Hardening per D-255(d), both mandatory
Per-body deep-rung cap: 512 Block+Chunk entries, enforced synchronously in put(), independent of the floor and byte budget. The ticket sanctions count-or-quota; count chosen as the direct D-226(d) information-content proxy.
Schema/version tag on every entry: project.yaml version (the D-192 co-ship boundary makes the client version the wire-schema version); exact-inequality mismatch = miss + drop, NEVER decode — checked in both has() and get_canvas().
Verification
30 new disk-cache tests + 7 request-integration + 3 sweep-wiring (restart persistence incl. floor flag, sweep independence both directions, cap semantics, version-mismatch never-decode asserted at the call level, corrupt/truncated/dangling-file recovery). Full client suite 3,440/3,440, 0 orphans, through the gate; cold-parse clean. A live restart-persistence eyeball runs next — merge waits on it.
What changes for the player
Revisiting a body across restarts is instant at every previously-visited rung; abandoned bodies quietly reclaim fine-grained space after ~2 weeks; the whole-body view of every visited body stays forever. Update-safe by construction.
## Summary
The final D-255 chain ticket: the Tier 2/3 disk-backed cache store per D-255(d) and the round-2 three-tier spec. step_canvas_disk_cache.gd stores the WIRE payload pre-decode (store_var/get_var round-trips the PNG PackedByteArrays natively — Image decode never runs in this file), keyed by the same composite key as Tier 1, with a per-body index.json and malformed-index recovery (rebuild-or-discard, never crash).
## Eviction — three independent mechanisms, exactly as ruled
- Rung-0 Global: retention floor, no sweep ever (also threaded into Tier 1 per the T-1182 handoff).
- Tier 2 geometry: byte-valid forever — evicted only by time-since-last-visit (14d starting tunable, on body-open) and LRU byte budget (256 MiB/body, 5-min coarse timer), two separate sweeps per the ruling.
- Tier 3 sim-state TTL: wired and tested; no production caller yet (no sim-state field exists on EncodedStepCanvas — the documented D-253 stub inheritance).
## Hardening per D-255(d), both mandatory
- Per-body deep-rung cap: 512 Block+Chunk entries, enforced synchronously in put(), independent of the floor and byte budget. The ticket sanctions count-or-quota; count chosen as the direct D-226(d) information-content proxy.
- Schema/version tag on every entry: project.yaml version (the D-192 co-ship boundary makes the client version the wire-schema version); exact-inequality mismatch = miss + drop, NEVER decode — checked in both has() and get_canvas().
## Verification
30 new disk-cache tests + 7 request-integration + 3 sweep-wiring (restart persistence incl. floor flag, sweep independence both directions, cap semantics, version-mismatch never-decode asserted at the call level, corrupt/truncated/dangling-file recovery). Full client suite 3,440/3,440, 0 orphans, through the gate; cold-parse clean. A live restart-persistence eyeball runs next — merge waits on it.
## What changes for the player
Revisiting a body across restarts is instant at every previously-visited rung; abandoned bodies quietly reclaim fine-grained space after ~2 weeks; the whole-body view of every visited body stays forever. Update-safe by construction.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
step_canvas_disk_cache.gd: the Tier 2/3 store per D-255(d) and the
round-2 three-tier spec. Payload is the WIRE, pre-decode — store_var/
get_var round-trips the PNG-encoded PackedByteArrays natively, and
Image.load_png_from_buffer never runs in this file. One composite key
shared verbatim with Tier 1 (hash filenames for filesystem safety);
per-body index.json with malformed-index recovery (rebuild-or-discard,
never crash).
Three independent eviction mechanisms, exactly as ruled: rung-0 Global
carries a retention floor no sweep touches (now also threaded into
Tier 1 per the T-1182 handoff); Tier 2 geometry is byte-valid forever
and evicts only by time-since-last-visit (14d starting tunable, on
body-open) and LRU byte budget (256 MiB/body, 5-min coarse timer) —
two separate sweeps; Tier 3 sim-state TTL is wired and tested but has
no production caller yet (no sim-state field exists on
EncodedStepCanvas — the D-253 stub inheritance, documented).
Hardening per D-255(d), both mandatory: per-body deep-rung cap
(512 Block+Chunk entries, enforced synchronously in put(), floor- and
budget-independent — the ticket sanctions count-or-quota; count chosen
as the direct D-226(d) information-content proxy) and a schema/version
tag on every entry (project.yaml version via the existing
loading_screen line-scan idiom — D-192 co-ship makes the client
version the wire-schema version; exact-inequality mismatch = miss +
drop, NEVER decode, checked in both has() and get_canvas()).
Integration: request_now() checks Tier 2 on a Tier-1 miss (synchronous
promote), Ready responses write through to both tiers, Pending never
writes; viewer runs the visit sweep on body-open + the background
sweep on a 5-min timer.
30 new disk-cache tests + 7 request-integration + 3 sweep-wiring tests
(restart persistence, sweep independence both directions, cap
semantics, version-mismatch never-decode, corrupt-index recovery).
Full client suite 3,440/3,440, 0 orphans; cold-parse clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Atomic index writes (tmp+rename), orphan-payload reconciliation sweep
folded into the background sweep, payload shape guard mirroring the
protocol's own width/height discriminator, size_bytes via get_position()
instead of a full payload re-read, and a 64-bit SHA-256 payload filename
(String.hash()'s 31-bit space made a silent wrong-map filename collision
a ~1-in-16k event per cap-full body; migration self-heals via the orphan
sweep). Tier-3 class doc rewritten to name the real D-253 seam — the
glaciation/flooded_q wire fields exist but carry static values and no
staleness signal crosses the wire; T-1190 tracks threading a wire TTL
into put(sim_ttl_sec) when the driving clock lands — replacing the false
'no sim-state wire field exists' premise. Four new tests plus a
_stub_canvas fixture helper centralizing the canvas shape contract.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reviewers: Hoshe (code quality) + Tyre (architecture), per the code-branch matrix.
Both returned REQUEST_CHANGES; every finding was verified against the branch source and either fixed or retracted with evidence. Fix commit: 51fe35d00.
Hoshe (code quality): REQUEST_CHANGES
#
File
Issue
Resolution
1
step_canvas_disk_cache.gd:258 _save_index
Truncate-in-place index write — crash mid-write corrupts index.json; recovery discards all entries, orphaning payloads
Fixed — atomic tmp+DirAccess.rename_absolute swap; failure path warns and removes the tmp (51fe35d00)
2
step_canvas_disk_cache.gd:345 put()
Payload-before-index write order + no directory reconciliation — orphaned .dat files leak forever, invisible to the byte-budget sweep
Fixed — new run_orphan_sweep() (index-referenced-basename set vs one directory listing) folded into run_background_sweep(); write order kept and documented as deliberate (orphan-payload is the recoverable direction); test added (51fe35d00)
3
step_canvas_disk_cache.gd:298 get_canvas()
Corrupted payload "can crash or hang the client" on get_var()
Partially retracted — get_var() returns null on a failed decode and the existing not canvas is Dictionary check catches it, with _drop_entry() deleting both index row and file: full self-heal, no crash path; allocation is bounded by file length. Residual fixed: guard strengthened to require width/height (the protocol's own discriminator), contract documented, garbage-bytes test added (51fe35d00)
4
step_canvas_disk_cache.gd:380 put()
get_file_as_bytes().size() re-reads the whole just-written payload for its byte count
Fixed — file.get_position() on the still-open handle; accuracy test added (51fe35d00)
Sim-state fields (glaciation/flooded_q) cached as never-stale geometry; doc claims "no sim-state WIRE field exists" citing round-1 — a live correctness gap
Partially retracted, doc fixed — the "live gap" scenario cannot occur today: the server hardwires flooded_q to 0 and ships glaciation as the static classification, byte-identical to an indefinitely-fresh derive with the TTL machinery explicitly wired ahead of D-253's driving clock (server step_canvas.rs module doc), and no staleness signal crosses the wire. TIER_GEOMETRY is therefore the correct classification today. Tyre's core complaint stands: the doc's premise was false and its citation superseded — rewritten to name the real seam, one-line note at the put() call site, and T-1190 filed to thread a wire TTL into the existing put(sim_ttl_sec) parameter when the D-253 clock lands (51fe35d00)
_payload_filename() used String.hash() & 0x7FFFFFFF — a 31-bit space in which two live keys colliding on a filename silently serve the wrong map as a valid hit (~1-in-16k per cap-full body). Replaced with 16-hex SHA-256 (64-bit space, negligible); the scheme change self-heals — old rows miss and drop, old files are orphan-swept. Included in 51fe35d00.
Verification
test_step_canvas_disk_cache 68/68 (64 existing — all kept green through the _stub_canvas fixture conversion — + 4 new), test_step_canvas_request 32/32, test_step_canvas_viewer 50/50; gdlint clean on both edited sources. Full suite runs at the push gate.
Restart-persistence eyeball (the merge gate from the PR description): see the follow-up comment with evidence.
Verdict: CHANGES REQUESTED -> addressed in 51fe35d00; merge follows the eyeball evidence.
# Review: t1183-client-cache -> main (type: code)
Reviewers: Hoshe (code quality) + Tyre (architecture), per the code-branch matrix.
Both returned REQUEST_CHANGES; every finding was verified against the branch source and either fixed or retracted with evidence. Fix commit: `51fe35d00`.
### Hoshe (code quality): REQUEST_CHANGES
| # | File | Issue | Resolution |
|---|------|-------|------------|
| 1 | step_canvas_disk_cache.gd:258 `_save_index` | Truncate-in-place index write — crash mid-write corrupts index.json; recovery discards all entries, orphaning payloads | **Fixed** — atomic tmp+`DirAccess.rename_absolute` swap; failure path warns and removes the tmp (`51fe35d00`) |
| 2 | step_canvas_disk_cache.gd:345 `put()` | Payload-before-index write order + no directory reconciliation — orphaned `.dat` files leak forever, invisible to the byte-budget sweep | **Fixed** — new `run_orphan_sweep()` (index-referenced-basename set vs one directory listing) folded into `run_background_sweep()`; write order kept and documented as deliberate (orphan-payload is the recoverable direction); test added (`51fe35d00`) |
| 3 | step_canvas_disk_cache.gd:298 `get_canvas()` | Corrupted payload "can crash or hang the client" on `get_var()` | **Partially retracted** — `get_var()` returns null on a failed decode and the existing `not canvas is Dictionary` check catches it, with `_drop_entry()` deleting both index row and file: full self-heal, no crash path; allocation is bounded by file length. Residual fixed: guard strengthened to require `width`/`height` (the protocol's own discriminator), contract documented, garbage-bytes test added (`51fe35d00`) |
| 4 | step_canvas_disk_cache.gd:380 `put()` | `get_file_as_bytes().size()` re-reads the whole just-written payload for its byte count | **Fixed** — `file.get_position()` on the still-open handle; accuracy test added (`51fe35d00`) |
### Tyre (architecture): REQUEST_CHANGES
| # | File | Issue | Resolution |
|---|------|-------|------------|
| 1 | step_canvas_disk_cache.gd:43 Tier-3 doc + request.gd:161 | Sim-state fields (`glaciation`/`flooded_q`) cached as never-stale geometry; doc claims "no sim-state WIRE field exists" citing round-1 — a live correctness gap | **Partially retracted, doc fixed** — the "live gap" scenario cannot occur today: the server hardwires `flooded_q` to 0 and ships `glaciation` as the static classification, byte-identical to an indefinitely-fresh derive with the TTL machinery explicitly wired ahead of D-253's driving clock (server step_canvas.rs module doc), and no staleness signal crosses the wire. TIER_GEOMETRY is therefore the *correct* classification today. Tyre's core complaint stands: the doc's premise was false and its citation superseded — rewritten to name the real seam, one-line note at the `put()` call site, and **T-1190** filed to thread a wire TTL into the existing `put(sim_ttl_sec)` parameter when the D-253 clock lands (`51fe35d00`) |
| 2 | step_canvas_disk_cache.gd:380 | Same double-read as Hoshe #4 | **Fixed** (same change) |
### Lead addition (beyond reviewer findings)
`_payload_filename()` used `String.hash() & 0x7FFFFFFF` — a 31-bit space in which two live keys colliding on a filename silently serve the wrong map as a valid hit (~1-in-16k per cap-full body). Replaced with 16-hex SHA-256 (64-bit space, negligible); the scheme change self-heals — old rows miss and drop, old files are orphan-swept. Included in `51fe35d00`.
### Verification
- `test_step_canvas_disk_cache` 68/68 (64 existing — all kept green through the `_stub_canvas` fixture conversion — + 4 new), `test_step_canvas_request` 32/32, `test_step_canvas_viewer` 50/50; gdlint clean on both edited sources. Full suite runs at the push gate.
- Restart-persistence eyeball (the merge gate from the PR description): see the follow-up comment with evidence.
### Verdict: CHANGES REQUESTED -> addressed in `51fe35d00`; merge follows the eyeball evidence.
Restart-persistence eyeball — PASS (the merge gate from the PR description)
Live protocol on the fixed tip 51fe35d00: cold client+server run visits Global/Region/District on GJ1c, clean quit, then a fresh client process + fresh server revisits the same rungs at the same centers against the untouched cache.
Restart serve-from-disk: the fresh process reports disk_entries(GJ1c)=3 at its first Global capture — before any wire round-trip was possible — and writes zero new entries across all three rungs. Payload files byte-untouched (same mtimes); only last_read_at bumped per entry: the read-touch signature of genuine Tier-2 hits.
Pixel identity: AE=0 (zero differing pixels) on all three rungs, run1 vs run2, via ImageMagick compare -metric AE and independently confirmed by identical SHA-256 per rung. Cross-process, cross-server, texel-exact.
Two anomalies surfaced and root-caused — neither a product bug: intermittent view-transform drift and a nav-stack pop were both traced (quantitatively: observed drift matched PAN_SPEED_PX_S × frame_time × held-frames to 0.02px) to real desktop input reaching the unfocused test window on this shared-desktop session. Neutralized in the harness with a verified input-suppression node (events intercepted AND zero resulting drift — positive proof, not absence-of-interference). One genuine low-priority product nit fell out: edge-scroll pans on mouse motion while the window is unfocused → T-1191. Harness techniques inventoried on T-1157.
Fidelity label: navigation was driven in-process via StepCanvasViewer's own methods (the same entry points its gdUnit suite exercises) rather than OS pointer injection, which this session's Wayland compositor blocks. Full production scene, real live server, real GPU rendering/capture — only the physical-pointer path was substituted, and that path is unchanged by this PR.
Gate status: pre-push suite green on the branch (3,448/3,448). Proceeding to merge.
# Restart-persistence eyeball — PASS (the merge gate from the PR description)
Live protocol on the fixed tip `51fe35d00`: cold client+server run visits Global/Region/District on GJ1c, clean quit, then a **fresh client process + fresh server** revisits the same rungs at the same centers against the untouched cache.
**Evidence chain:**
- **Cold write:** 3 disk entries (`disk_entries` 1→2→3), 16-hex sha256 payload filenames, `schema_version "0.4.0"` matching project.yaml exactly, Global `retention_floor=true` (Region/District false), 36,936,864 bytes total.
- **Restart serve-from-disk:** the fresh process reports `disk_entries(GJ1c)=3` at its *first* Global capture — before any wire round-trip was possible — and writes zero new entries across all three rungs. Payload files byte-untouched (same mtimes); only `last_read_at` bumped per entry: the read-touch signature of genuine Tier-2 hits.
- **Pixel identity:** AE=0 (zero differing pixels) on all three rungs, run1 vs run2, via ImageMagick `compare -metric AE` and independently confirmed by identical SHA-256 per rung. Cross-process, cross-server, texel-exact.
**Two anomalies surfaced and root-caused — neither a product bug:** intermittent view-transform drift and a nav-stack pop were both traced (quantitatively: observed drift matched `PAN_SPEED_PX_S × frame_time × held-frames` to 0.02px) to **real desktop input reaching the unfocused test window** on this shared-desktop session. Neutralized in the harness with a verified input-suppression node (events intercepted AND zero resulting drift — positive proof, not absence-of-interference). One genuine low-priority product nit fell out: edge-scroll pans on mouse motion while the window is unfocused → **T-1191**. Harness techniques inventoried on **T-1157**.
**Fidelity label:** navigation was driven in-process via StepCanvasViewer's own methods (the same entry points its gdUnit suite exercises) rather than OS pointer injection, which this session's Wayland compositor blocks. Full production scene, real live server, real GPU rendering/capture — only the physical-pointer path was substituted, and that path is unchanged by this PR.
Gate status: pre-push suite green on the branch (3,448/3,448). Proceeding to merge.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
The final D-255 chain ticket: the Tier 2/3 disk-backed cache store per D-255(d) and the round-2 three-tier spec. step_canvas_disk_cache.gd stores the WIRE payload pre-decode (store_var/get_var round-trips the PNG PackedByteArrays natively — Image decode never runs in this file), keyed by the same composite key as Tier 1, with a per-body index.json and malformed-index recovery (rebuild-or-discard, never crash).
Eviction — three independent mechanisms, exactly as ruled
Hardening per D-255(d), both mandatory
Verification
30 new disk-cache tests + 7 request-integration + 3 sweep-wiring (restart persistence incl. floor flag, sweep independence both directions, cap semantics, version-mismatch never-decode asserted at the call level, corrupt/truncated/dangling-file recovery). Full client suite 3,440/3,440, 0 orphans, through the gate; cold-parse clean. A live restart-persistence eyeball runs next — merge waits on it.
What changes for the player
Revisiting a body across restarts is instant at every previously-visited rung; abandoned bodies quietly reclaim fine-grained space after ~2 weeks; the whole-body view of every visited body stays forever. Update-safe by construction.
🤖 Generated with Claude Code
Review: t1183-client-cache -> main (type: code)
Reviewers: Hoshe (code quality) + Tyre (architecture), per the code-branch matrix.
Both returned REQUEST_CHANGES; every finding was verified against the branch source and either fixed or retracted with evidence. Fix commit:
51fe35d00.Hoshe (code quality): REQUEST_CHANGES
_save_indexDirAccess.rename_absoluteswap; failure path warns and removes the tmp (51fe35d00)put().datfiles leak forever, invisible to the byte-budget sweeprun_orphan_sweep()(index-referenced-basename set vs one directory listing) folded intorun_background_sweep(); write order kept and documented as deliberate (orphan-payload is the recoverable direction); test added (51fe35d00)get_canvas()get_var()get_var()returns null on a failed decode and the existingnot canvas is Dictionarycheck catches it, with_drop_entry()deleting both index row and file: full self-heal, no crash path; allocation is bounded by file length. Residual fixed: guard strengthened to requirewidth/height(the protocol's own discriminator), contract documented, garbage-bytes test added (51fe35d00)put()get_file_as_bytes().size()re-reads the whole just-written payload for its byte countfile.get_position()on the still-open handle; accuracy test added (51fe35d00)Tyre (architecture): REQUEST_CHANGES
glaciation/flooded_q) cached as never-stale geometry; doc claims "no sim-state WIRE field exists" citing round-1 — a live correctness gapflooded_qto 0 and shipsglaciationas the static classification, byte-identical to an indefinitely-fresh derive with the TTL machinery explicitly wired ahead of D-253's driving clock (server step_canvas.rs module doc), and no staleness signal crosses the wire. TIER_GEOMETRY is therefore the correct classification today. Tyre's core complaint stands: the doc's premise was false and its citation superseded — rewritten to name the real seam, one-line note at theput()call site, and T-1190 filed to thread a wire TTL into the existingput(sim_ttl_sec)parameter when the D-253 clock lands (51fe35d00)Lead addition (beyond reviewer findings)
_payload_filename()usedString.hash() & 0x7FFFFFFF— a 31-bit space in which two live keys colliding on a filename silently serve the wrong map as a valid hit (~1-in-16k per cap-full body). Replaced with 16-hex SHA-256 (64-bit space, negligible); the scheme change self-heals — old rows miss and drop, old files are orphan-swept. Included in51fe35d00.Verification
test_step_canvas_disk_cache68/68 (64 existing — all kept green through the_stub_canvasfixture conversion — + 4 new),test_step_canvas_request32/32,test_step_canvas_viewer50/50; gdlint clean on both edited sources. Full suite runs at the push gate.Verdict: CHANGES REQUESTED -> addressed in
51fe35d00; merge follows the eyeball evidence.Restart-persistence eyeball — PASS (the merge gate from the PR description)
Live protocol on the fixed tip
51fe35d00: cold client+server run visits Global/Region/District on GJ1c, clean quit, then a fresh client process + fresh server revisits the same rungs at the same centers against the untouched cache.Evidence chain:
disk_entries1→2→3), 16-hex sha256 payload filenames,schema_version "0.4.0"matching project.yaml exactly, Globalretention_floor=true(Region/District false), 36,936,864 bytes total.disk_entries(GJ1c)=3at its first Global capture — before any wire round-trip was possible — and writes zero new entries across all three rungs. Payload files byte-untouched (same mtimes); onlylast_read_atbumped per entry: the read-touch signature of genuine Tier-2 hits.compare -metric AEand independently confirmed by identical SHA-256 per rung. Cross-process, cross-server, texel-exact.Two anomalies surfaced and root-caused — neither a product bug: intermittent view-transform drift and a nav-stack pop were both traced (quantitatively: observed drift matched
PAN_SPEED_PX_S × frame_time × held-framesto 0.02px) to real desktop input reaching the unfocused test window on this shared-desktop session. Neutralized in the harness with a verified input-suppression node (events intercepted AND zero resulting drift — positive proof, not absence-of-interference). One genuine low-priority product nit fell out: edge-scroll pans on mouse motion while the window is unfocused → T-1191. Harness techniques inventoried on T-1157.Fidelity label: navigation was driven in-process via StepCanvasViewer's own methods (the same entry points its gdUnit suite exercises) rather than OS pointer injection, which this session's Wayland compositor blocks. Full production scene, real live server, real GPU rendering/capture — only the physical-pointer path was substituted, and that path is unchanged by this PR.
Gate status: pre-push suite green on the branch (3,448/3,448). Proceeding to merge.
Pull request closed