Status: defect on main at eb2c06e2; diagnosis, mechanism and fix measured. Filed 2026-09-22 from the run that found it. history-stride10 is a registered admission row outside the 217 (#190's lane), so this blocks that lane's evidence on this tree.
1. What fails
history-stride10 no longer completes:
$ LAYERFS_CONSTRUCTION_WORKERS=1 LAYERFS_HISTORY_ADVISORY=1 LAYERFS_HISTORY_DEPTH_LIMIT=7 \
fs-bench-storage-content --case history-stride10 --corpus <corpus> --out /tmp/x
history-stride10 INCOMPLETE gates=1
g1.o1-chain-complete INCOMPLETE product error: Integrity("dependency encoded work")
Deterministic: three fresh runs, ~3.5–5.3 s wall, abort at 17.1 MB of Store (4,041 pages, apparent_bytes 16,551,936 in all three). Also reproduced through runner.py perf (benchmark-results/history-stride10/stride10-20260922T121313Z, repeat-20260922T121442Z, stride10-l7-20260922T121536Z).
It is not the budget, not the corpus (history_corpus.self_check clean: 17 states / 101,477 path states / 561,010,345 B), and not the history switches: LAYERFS_HISTORY_ADVISORY=1 LAYERFS_HISTORY_DEPTH_LIMIT=7 and LAYERFS_HISTORY_CHUNK_PREDECESSORS=0 all abort. The harness's own documented refusal (advisory depth u8::MAX, "the product currently rejects") is a different one.
2. It passes on every archived build
Same corpus, same declared switches:
| build |
result |
operation |
Store |
stage-6-history-190-opt-20260919T232858Z/binaries/baseline-c5826d20… (Sep 19) |
PASS 17/17 |
46.70 s |
49.4 MB |
stage-6-history-209-merged-20260920T225623Z/binary-archive/merged-main (Sep 20) |
PASS 17/17 |
23.54 s |
52.2 MB |
main @ eb2c06e2 (today) |
INCOMPLETE |
— |
aborts at 17.1 MB |
3. Mechanism
record_width charges a chain read the group's width for the whole-file lane (core/crates/layerfs-storage/src/encoding/delta/read.rs:527), while the budget it is charged against is per chain — CHAIN_ENCODED_LIMIT = 256 KiB (policy.rs:183). That early return was written when the lane sealed one record per group, so group width ≡ record width. a1faf957e ("group the compact whole-file lane") gave the lane whole groups — its own census puts it at 519 groups instead of 9,444 — so every chain read is now charged the group's membership, and a depth-8 chain asks for roughly eight group targets (~384 KiB) against 256 KiB.
Verified by construction: restoring the pre-a1faf957e seal rule (PackLane::WholeFile => occupied) in a temporary diagnostic build makes the same case pass — 15.63 s, 17/17 states, 71.1 MB. The edit was reverted immediately; the tree is clean and the binary hashes back to 90a04b44….
Secondary: on the abort path phases-perf.json reports preparation + verification larger than the invocation wall (8.75 s inside 5.34 s), so g7.phase-reconciliation fires. The gate is right; the document is wrong when the driver aborts.
4. Fix (owner-authorized option A), implemented and measured
record_width no longer special-cases the lane: it takes the Raw arm every other lane takes and charges the record its locator names. The lane seals GroupCodec::Raw by construction (pack/assemble.rs:185), so that arm is exact for it.
- Production LOC 97,249 → 97,246 (delta −3); the added lines are the explaining comment.
history-stride10: PASS 17/17, operation 15.06 s, and all 17 state roots identical to the Sep-19, Sep-20 and diagnostic runs.
history-stride3: PASS 53/53, 37.10 s, 106.7 MB.
pipeline-namespace-100000: 15/15 gates, 14/14 pinned counters, digest 2412681d… unchanged (statements 12,206 / commits 388 / inserted 113,635 / content_bytes 502,914,928), i.e. no other row's pin moves.
- 110 test binaries, 631 passed / 0 failed; clippy clean; fmt clean;
core/tools/check_product_boundary.py PASS.
The cost, stated plainly. Nine objects change representation — delta.prefix_selected 38,117 → 38,108 and delta.full_records 12,998 → 13,007 — because the reader's charge feeds the selection's own chain budget. The row is therefore not byte-identical to the pre-grouping arm; its 17 roots are.
Option B (charge only what was fetched) is not taken. A alone cleared the lane, and B changes more semantics ("encoded bytes this wave read") for no measured need. It stays in the priced list rather than in the diff.
5. Why the crate tests did not catch it
chain_encoded_limit is a constant with no test-visible override, the whole-file chain depth caps at 8, and the closest expressible in-crate shape charges 240,607 of 262,144 — 8 % short of the refusal. So the lane is the only oracle that reaches this boundary, and that is recorded rather than papered over. A future round that wants a unit test here needs a test-visible capacity override first.
6. Not in this issue
- The store-size axis of grouping on this lane (63.3 MB with the fix, 71.1 MB with one-record groups on the same tree, 52.2 MB on the Sep-20 build) is measured but not attributed: the three differ in more than the seal rule.
history-stride1 was not run (157 states, the campaign's own budget ruling keeps it out of these rounds).
- Raising
CHAIN_ENCODED_LIMIT is not proposed: it is a bounded-work guarantee with its own test (tests/delta_chains.rs:83).
Evidence: docs/roadmap/0.1/0.1.7/evidence/issue228-stride10-chain-charge-20260922T124500Z/report.md (runs under benchmark-results/history-stride10/ and benchmark-results/postfix/).
1. What fails
history-stride10no longer completes:Deterministic: three fresh runs, ~3.5–5.3 s wall, abort at 17.1 MB of Store (4,041 pages,
apparent_bytes16,551,936 in all three). Also reproduced throughrunner.py perf(benchmark-results/history-stride10/stride10-20260922T121313Z,repeat-20260922T121442Z,stride10-l7-20260922T121536Z).It is not the budget, not the corpus (
history_corpus.self_checkclean: 17 states / 101,477 path states / 561,010,345 B), and not the history switches:LAYERFS_HISTORY_ADVISORY=1 LAYERFS_HISTORY_DEPTH_LIMIT=7andLAYERFS_HISTORY_CHUNK_PREDECESSORS=0all abort. The harness's own documented refusal (advisory depthu8::MAX, "the product currently rejects") is a different one.2. It passes on every archived build
Same corpus, same declared switches:
stage-6-history-190-opt-20260919T232858Z/binaries/baseline-c5826d20…(Sep 19)stage-6-history-209-merged-20260920T225623Z/binary-archive/merged-main(Sep 20)main@eb2c06e2(today)3. Mechanism
record_widthcharges a chain read the group's width for the whole-file lane (core/crates/layerfs-storage/src/encoding/delta/read.rs:527), while the budget it is charged against is per chain —CHAIN_ENCODED_LIMIT = 256 KiB(policy.rs:183). That early return was written when the lane sealed one record per group, so group width ≡ record width.a1faf957e("group the compact whole-file lane") gave the lane whole groups — its own census puts it at 519 groups instead of 9,444 — so every chain read is now charged the group's membership, and a depth-8 chain asks for roughly eight group targets (~384 KiB) against 256 KiB.Verified by construction: restoring the pre-
a1faf957eseal rule (PackLane::WholeFile => occupied) in a temporary diagnostic build makes the same case pass — 15.63 s, 17/17 states, 71.1 MB. The edit was reverted immediately; the tree is clean and the binary hashes back to90a04b44….Secondary: on the abort path
phases-perf.jsonreportspreparation + verificationlarger than the invocation wall (8.75 s inside 5.34 s), sog7.phase-reconciliationfires. The gate is right; the document is wrong when the driver aborts.4. Fix (owner-authorized option A), implemented and measured
record_widthno longer special-cases the lane: it takes theRawarm every other lane takes and charges the record its locator names. The lane sealsGroupCodec::Rawby construction (pack/assemble.rs:185), so that arm is exact for it.history-stride10: PASS 17/17, operation 15.06 s, and all 17 state roots identical to the Sep-19, Sep-20 and diagnostic runs.history-stride3: PASS 53/53, 37.10 s, 106.7 MB.pipeline-namespace-100000: 15/15 gates, 14/14 pinned counters, digest2412681d…unchanged (statements12,206 /commits388 /inserted113,635 /content_bytes502,914,928), i.e. no other row's pin moves.core/tools/check_product_boundary.pyPASS.The cost, stated plainly. Nine objects change representation —
delta.prefix_selected38,117 → 38,108 anddelta.full_records12,998 → 13,007 — because the reader's charge feeds the selection's own chain budget. The row is therefore not byte-identical to the pre-grouping arm; its 17 roots are.Option B (charge only what was fetched) is not taken. A alone cleared the lane, and B changes more semantics ("encoded bytes this wave read") for no measured need. It stays in the priced list rather than in the diff.
5. Why the crate tests did not catch it
chain_encoded_limitis a constant with no test-visible override, the whole-file chain depth caps at 8, and the closest expressible in-crate shape charges 240,607 of 262,144 — 8 % short of the refusal. So the lane is the only oracle that reaches this boundary, and that is recorded rather than papered over. A future round that wants a unit test here needs a test-visible capacity override first.6. Not in this issue
history-stride1was not run (157 states, the campaign's own budget ruling keeps it out of these rounds).CHAIN_ENCODED_LIMITis not proposed: it is a bounded-work guarantee with its own test (tests/delta_chains.rs:83).Evidence:
docs/roadmap/0.1/0.1.7/evidence/issue228-stride10-chain-charge-20260922T124500Z/report.md(runs underbenchmark-results/history-stride10/andbenchmark-results/postfix/).