Skip to content

Keep the record of set_input values in step with storage - #561

Merged
MaxGhenis merged 16 commits into
masterfrom
fix-user-input-keys-drift
Oct 9, 2026
Merged

MaxGhenis merged 16 commits into
masterfrom
fix-user-input-keys-drift

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #559.

Problem and behavior

Deleting an input could leave _user_input_keys pointing at a later calculation for the same variable, branch, and period. apply_reform then preserved that calculation as an input, and to_input_dataframe exported it. Copies also shared the record despite having separate storage, and calculating set_input handlers could register their intermediate calculations.

The mixed-storage witness makes the provenance problem explicit: January rent is calculated to disk as 0, then supplied in memory as 500. Deleting the year removes the memory input while disk storage retains the calculation. The disk 0 must not become a replayable input. A genuine disk input, however, must survive deletion of a newer memory input.

This PR changes outputs when a reform follows input deletion and the stale record would previously have preserved a calculation or default. Country constructor flows that move inputs such as employment_income make that behavior relevant to downstream validation.

Change

  • Keep the existing (variable, branch, Period) record, and track the memory/disk tiers that received an actual input in Holder._user_input_storage. _record_input_storage updates provenance when a tier is written; _stores_user_input checks surviving input locations. A cached value in another tier cannot stand in for a deleted input.
  • Holder.delete_arrays compares storage keys before and after deletion, then removes a recorded slot when no input tier survives. Simulation.delete_arrays applies this to the visible branches. Deletion does not load disk arrays to make that decision.
  • _seed_input_storage handles inputs registered without tier metadata, including Restore inputs as inputs, evict the fast cache on holder writes, reject non-numeric uprating and defined_for #576's restore path. Existing storage input marks can seed an already-recorded slot; they do not register an ordinary cache value for export or reform replay.
  • Reform replay consults the recorded tiers before choosing a value. An unrecorded memory cache cannot mask a genuine disk input. If a cache overwrite destroyed the only supplied input, replay drops the stale record instead of promoting that cache to an input. The registry is rebuilt from exactly the preserved snapshots. Non-derived external cache writes seed an existing register-only input's metadata before overwriting its tier, including restored inputs that did not yet have holder metadata.
  • Normalize periods from their fields so the record names the actual storage alias: eternity, calendar and rolling twelve-month years, and valid early-year periods. This avoids reparsing an early-year storage string after the value has already been written.
  • Give clones and branches independent input records, tier metadata, and running set_input contexts. Rebuild the input record during subsampling.
  • Exclude put_in_cache writes from input registration and input-context branch redirection. Custom handlers store replayable inputs through holder._set or holder.set_input. The Holder.set_input docstring and changelog describe this handler contract and behavior change.

The storage-level derived mark remains distinct from replay provenance. _set preserves the existing positional derived argument and adds is_input after it. The record's tuple shape remains compatible with downstream readers such as PE-US _rebind_holders.

Invariants and regression coverage

For the tested set_input, calculation, deletion, clone/branch, and cache-invalidation operations:

  1. The record names surviving supplied inputs, with provenance tracked independently in each storage tier. A retained disk calculation does not replace a deleted memory input; a retained genuine disk input does survive.
  2. The record, stored values, and dataframe exports agree with a reference model seeded from supplied inputs. Formula results calculated inside input handlers are excluded.
  3. Copies have independent records and tier metadata. Updating one simulation does not change another's input record.
  4. Period normalization is idempotent and matches storage keys, including eternity and twelve-month aliases, without rejecting valid early years.
  5. Reform replay retains recorded inputs and purges unrecorded calculations and broader carry-over marks.

These claims apply to the operations and storage behavior exercised by the tests; they are not a claim about every possible sequence. In particular, they do not guarantee preservation through pre-existing input-overwriting behavior such as calculate_add on a two-month input. This PR does not repair that separate storage behavior.

The new external-cache differential checks post-reform records, exports, supplied values, and repeated reform against a fresh simulation, including optional cloning. Its focused invariants are:

  • A register-only restored slot seeds only its existing non-derived tiers before a holder-mediated overwrite; an unregistered cache cannot create an input record.
  • After invalidation, the registry equals the preserved input snapshots. A slot whose only input was overwritten leaves no ghost key, export, or replay value.
  • Repeating a no-op reform on the simulation or its clone leaves the same input record, exports, and calculated values as a fresh simulation containing the surviving inputs.

These assertions do not prove that an export made before reform ignores an arbitrary external cache overwrite. Direct storage mutations that bypass holder provenance remain outside that contract.

The existing #576 persistence contract is distinct: dump/restore deliberately registers every non-derived dumped value as a new leaf input, including an external cache value that live reform replay would drop. The restore differential preserves round-trip values, marks, and pre-reform answers, then checks live replay against an independent supplied-write reference and restored replay against the dump's non-derived value snapshot. These post-reform checks apply to every generated trace, including an explicit same-value input/cache overwrite; cross-simulation equality applies when those independent expected input snapshots agree. The #576 dump/restore input-registration contract is unchanged.

Existing example, property, and differential coverage is retained:

  • test_user_input_keys_match_reference_model exercises families of simulations with memory storage, including handler calculations, exports, clones, and branches: 300 sequences of up to 30 operations.
  • test_user_input_keys_follow_memory_and_disk_storage tracks supplied values independently in each tier and checks records, reads, and actual exports: 200 sequences of up to 25 operations, with an explicit mixed-storage counterexample.
  • test_period_normalization_matches_storage_strings checks normalization against storage strings and the existing parser where applicable, with explicit calendar and rolling twelve-month examples.
  • test_tier_provenance_replays_the_same_inputs_as_a_fresh_simulation compares mixed-storage deletion and reform replay against a fresh simulation containing only the surviving supplied inputs.
  • Focused examples retain the prior soundness witnesses, early-year matrix, genuine-disk-input control, and register-only restore compatibility.

Fix-round regression modules:

  • tests/core/test_input_tier_invalidation.py: genuine disk inputs versus unrecorded memory caches, overwritten-only record removal, register-only restored inputs, optional cloning, and the fresh-simulation differential across repeated reform (60 generated examples plus the explicit disk-input witness).
  • tests/core/test_registered_input_deletion.py: register-only deletion for early years, plus a differential with set_input across period units, sizes, aliases, storage tiers, and underscored branches. Deleting the slot removes the record; a later cache write is purged by reform. At the reviewed source head, the newly added regressions produced 38 failed, 16 passed before the repair.
  • tests/core/test_disk_period_filenames.py: compound and early-year disk periods under emulated Windows filename restrictions, replacement/clone paths, legacy restore and current-file precedence. Its memory/disk differential compares logical branch-period keys, values, and live derived marks after writes; source values remain independent of clone writes, and restore reads the original file values. Restore retains its existing behavior of clearing derived marks. At the reviewed source head, these newly added regressions produced 22 failed, 3 passed before the repair; the legacy/separator-free restore controls passed.

All three modules were exercised against the reviewed implementation at 7559e6952657d9c1af276c8268017ce09561d28b before their repairs and rerun afterward:

Regression module Before repair After repair
Input-tier invalidation 5 failed 5 passed
Registered-input deletion 38 failed, 16 passed 54 passed
Portable disk filenames 22 failed, 3 passed 25 passed

The filename controls and modern-year deletion controls passed before the fixes. These three after-repair results were obtained at a4f384d77ed0607b6e74e5d269dcbf09c979c50d; tier invalidation also passed all five tests after base reconciliation. The new property/differential invariants are stated above; existing coverage was retained.

Validation

Final head: 5a69ca8d835484fc582a6c638c231da7c9af2001.

The final head includes canonical master at 648f4e467d3c20afd206ce5f9be7f89752809257 through a merge commit, preserving history and the required #576-before-#561 order. The holder conflict was reconciled by retaining both #576's unconditional fast-cache eviction and #561's supplied-input provenance updates. Incoming restore, copy/pickle, and numeric-validation behavior was retained. The intermediate repair head conflicted with the updated base, so GitHub did not start its PR workflow; the final head is mergeable and started a fresh workflow.

Each file was run directly, in the foreground, one at a time with uv run --no-sync --python 3.13 pytest -q --tb=short; the three initial regression modules additionally used --noconftest. The replay-index check selected the single named test below. PYTHONPATH was set to the checkout root and PYTHONNOUSERSITE=1; uv selected a fresh isolated environment and a writable local cache. No lock wrapper, folder run, full suite, or microsimulation was used.

Environment: macOS arm64, CPython 3.13.9, NumPy 2.4.2, pytest 9.1.1, Hypothesis 6.168.3, Ruff 0.15.5, Plotly 5.24.1. Dependencies were installed into the isolated environment, not borrowed through another environment's .pth file. Import-origin and search-path checks confirmed core loads from the checkout and test/runtime packages from that environment. Editable package metadata was refreshed to core 3.32.25 after the base update; uv pip check verified all 65 installed packages are compatible.

In addition to the three regression modules above, these focused checks passed on the combined code tree (production code at f38247002d2c81939be9d848ff61447a71123eed, with the subsequent test-only follow-up now committed in the final head):

File or selected test Result
tests/core/test_user_input_keys.py 29 passed
tests/core/test_user_input_keys_property.py 3 passed
tests/core/test_user_input_keys_soundness.py 67 passed
tests/core/test_apply_reform_preserves_user_inputs.py 9 passed
tests/core/test_on_disk_storage_clones.py 27 passed
tests/core/test_storage_input_index.py::test_apply_reform_keeps_only_the_replayed_inputs 1 passed
tests/core/test_holder_write_fast_cache.py 16 passed
tests/core/test_input_tier_invalidation.py (rerun after reconciliation) 5 passed
tests/core/test_restore_input_registry_property.py 1 passed
tests/core/test_restore_input_provenance.py 2 passed
tests/core/test_simulation_copy_pickle_property.py 2 passed

The retained restore property first failed after base reconciliation on the explicit-input then identical-cache overwrite trace. Its old key/mark-only comparison assumed equal replay provenance. The corrected independent model passes with the same 200-example budget and operation strategies, retains round-trip and fresh-input comparisons, and adds unconditional live and restored replay checks plus a fresh serialized-leaf reference for every post-reform request. The two deterministic controls pin both same-value and changed-value overwrites across repeated reform. This fixes the test oracle while preserving #576's documented persistence behavior.

Focused Ruff lint and format checks passed for all eight edited Python files; git diff --check passed. No mutation campaign or performance benchmark was rerun.

At the reviewed head 7559e6952657d9c1af276c8268017ce09561d28b, CI run 37847864480 has 15 successful checks and four failed Windows Test jobs, for Python 3.11–3.14. All four Windows jobs fail exactly the same eight early-year multi-day cases: disk storage, the simulation/holder entry points, and years 1, 99, 999, and 1000. The traceback reaches numpy.save with a basename such as default_day:1-03-17:2.npy and raises OSError: [Errno 22] Invalid argument. Colon is reserved in Windows filenames. Microsoft file naming conventions.

The storage repair now encodes colons as semicolons only in the physical filename's period suffix, including replacement and clone-write filenames. Logical storage keys remain unchanged. Restore reads legacy colon filenames, decodes the new representation without changing branch semicolons or underscores, and prefers the current portable file when a legacy file remains beside an updated value. Valid serialized periods contain no semicolons, so this encoding is reversible without lengthening filenames. Existing live readers still indexed to a legacy file see its previous value until they restore again. Filename compatibility is one-way: current Core restores legacy files, but older Core readers do not decode the new compound filename encoding.

The portable-filename regression module passes all 25 cases locally. The existing early-year cases are retained; native Windows verification is reported separately below.

Final-head CI at 5a69ca8d83 passed: all 19 checks succeeded. These include lint, the changelog check, the test matrix on ubuntu and native Windows for Python 3.11–3.14, and "Test Core and country packages" on both platforms. The native Windows jobs that failed at 7559e695 with OSError: [Errno 22] on colon filenames now pass.

A separate description evidence check failed seven targeted assertions against the posted body at the reviewed head: tier tracking, qualified sequence claims, finished review status, the explicit Impact section, the fresh-impact HOLD, the Windows failures, and removal of the stale performance table. These are documentation-only checks of the description, not production regression tests or evidence that population-impact gates have passed.

The completed description passes all nine targeted evidence assertions, including the Impact/HOLD, current tier design, qualified invariants, CI disclosure, removal of stale performance claims, and absence of local paths or unfinished placeholders.

No new performance benchmark or mutation campaign was run in this fix round. The former performance table and mutation totals were measured on earlier heads and do not measure the current tier-tracking implementation. Writes after a holder has stored an input do period normalization and provenance bookkeeping. Seeding register-only inputs before overwrite also adds normalization on the external non-derived cache-write path when the simulation has registered inputs; the added condition does not change ordinary core writes with derived=True. This description makes no measured cost claim for those writes. The focused no-disk-read deletion regressions remain, but they do not establish performance for all recording paths.

Full suites, documentation builds, country-wide tests, and microsimulations were not run locally in this fix round; CI and the hub own those gates. Documentation changes cover the handler contract in Holder.set_input and the existing input changelog fragment, and the portable filename/legacy restore contract in the storage class and restore docstrings plus a storage changelog fragment. The API reference renders those source docstrings; no generated documentation is hand-edited. Documentation build validation remains a CI gate.

Documentation review: impact medium, confidence medium. The durable source documentation describes the changed handler and persistence contracts; no new public import or developer workflow needs separate documentation. Focused Ruff and description checks validate the edited text and code formatting. Documentation builds were not run locally. The older-reader filename limitation is explicit above, and the PR-only review and impact judgments stay in this description.

Review status

The round-3 code approval didn't supersede the soundness r3b UNSOUND verdict at 7aeaac0e95dee171cb15a371f1b425849d6a4436. The mixed-tier provenance and early-year repairs were then reviewed at 7559e6952657d9c1af276c8268017ce09561d28b. That delta review found no P1 and requested description, impact and CI corrections.

The round-2 delta review at 5a69ca8d83 (static read, Opus via Subfleet): APPROVE. All three earlier P2s are resolved. Its P3s were the stale CI text (fixed here) and two limits that are already disclosed: the compound-period filename format, which older Core can't read, and an external-cache export edge case before a reform.

Impact

Hub A/B microsimulation, PE-US, current head. The hub ran PolicyEngine-US main 8b8ae4cc12 on the default dataset twice for each year. One run used Core at the merge base 648f4e467d; the other used this PR's head 5a69ca8d83. The runs went one at a time under the shared heavy-job lock.

Output 2025 change 2026 change Records changed
household_net_income 0 0 0
income_tax 0 0 0
state_income_tax 0 0 0
household_benefits 0 0 0
household and person-household IDs 0 0 0

Every compared array is identical. Run time was also unchanged: 178s on both cores for 2025, and 187s (base) against 172s (PR) for 2026. The peak footprint was the same on both cores, at 11,264 MB (2025) and 12,288 MB (2026).

Not run: a PE-UK microsimulation. The PR changes country-agnostic holder bookkeeping. PE-UK is exercised only by CI's "Test Core and country packages" jobs, which pass. No partner baseline files change.

The historical results (PE-UK eFRS 2026, 36/36 arrays identical; PE-US eCPS, 3,000 households and marginal rates, 25/25 identical) predate the tier-tracking logic. The PE-US A/B above replaces them for this head.

Dependencies and rulings

axiom: n/a: simulation infrastructure; no policy rule or statute.

Simulation._user_input_keys records each (variable, branch, period) stored
through set_input. _invalidate_all_caches (run by apply_reform) keeps the
values it names, to_input_dataframe exports them, and country packages read
it to tell an entered value from a calculated one. It drifted from storage
in two ways (#559):

- delete_arrays deleted the values but kept their entries, so a formula
  result calculated later for the same period counted as an input: it
  survived apply_reform and was exported. Holder.delete_arrays, which
  Simulation.delete_arrays calls for each branch it deletes from, now drops
  the entries for the variable, that branch and the periods in-memory
  storage deletes (all of them for an eternal variable). Code that deletes
  through the holder, as country packages do when they move an input to
  another variable, is covered too. Disk storage deletes only the period
  asked for, so the entry for a value it still holds is kept.
- clone (so also get_branch) shared the record between simulations that
  store their values separately, so an input set on a clone, on a branch's
  parent after the branch was made, or on the original after cloning was
  recorded for both. The copy now gets its own record, and its own empty
  list of running set_input calls.

Tests: example regressions (11 of 12 fail before the fix; the twelfth
guards against dropping too much), and a Hypothesis property that runs
random set_input / calculate / delete_arrays / clone / get_branch /
_invalidate_all_caches sequences against a reference model of each
simulation's inputs.

Fixes #559

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follow-up to the review of the first commit:

- Holder.delete_arrays no longer looks through the whole record. It
  compares the periods memory stores for the branch before and after the
  deletion and discards exactly those entries, so its cost does not grow
  with the record. Country marginal-rate code deletes every variable on a
  branch: with 9,000 entries and 3,024 variables the loop took 0.73 s with
  the scan and takes 0.018 s now (0.004 s without any pruning). A holder
  with disk storage, which cannot list its periods for every branch name,
  still looks through the record for the variable's entries in the deleted
  periods and drops those whose value neither storage holds.
- Holder._set records the period as storage keys the value: eternity for an
  eternal variable whatever period it was set for, and a Period for a
  handler that passes a string. Each entry names one stored value, so a
  string-period input is exported and deleting an eternal input drops its
  entry.
- put_in_cache stores with is_input=False (the same keyword as #560), so
  values a custom set_input handler calculates are formula results, which
  apply_reform recalculates, not inputs.
- subsample starts the record again before it rebuilds the simulation, so it
  records only what the rebuild stores.

Tests: a guard that a memory-only deletion never iterates the record; the
eternal, string-period, calculating-handler, disk and subsample cases; the
property's model now includes a handler that calculates and stores months
under string periods.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 2 commits October 2, 2026 08:07
…s as stored

Round-2 review fixes:
- An entry is dropped only when neither storage still holds its value, so an
  input deleted from memory that survives on disk stays an input.
- Twelve months starting on the first of a month are recorded as the year
  storage keys them under, so deleting that year drops the entry.
- Deleting compares the keys each storage holds before and after: it no
  longer goes through the record or loads files for a disk-backed holder.
- The property model is seeded from the situation, checks to_input_dataframe
  itself, and a second property covers memory and disk storage.
- Regression for the carry-over case found in the review of #562.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 3, 2026
…ly for read branches

Review round 3:

- Holder._set no longer drops a record entry when a calculated value is
  stored over an input. A calculate_add at a variable's own period stores
  the unchanged input and lost its record; a clone, which shares its
  source's record until #561, deleted the source's entries; and ETERNITY
  variables keep entries under several periods. The case the cleanup was
  for, calculate_add caching a sum over an input, is what #571 stops.
- Master now gives a storage that shares nothing a frozenset for _shared
  (#578). The drop step deleted from it directly; it now leaves that to
  InMemoryStorage._stop_sharing_dropped_keys.
- The helpers evicted calculate's fast cache for a sub-period stored under
  any branch name, including one the simulation does not read. They now
  evict only for branches it reads, as #576's holder-write property
  requires.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 5 commits October 7, 2026 17:13
…eriods safely

Preserve replayed-input tuples and master carry-over marks; add r3b soundness witnesses and value-aware disk properties.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seed existing recorded input locations before writes, retain positional derived semantics, and validate period aliases and early-year replay and deletion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis merged commit 0d1d154 into master Oct 9, 2026
19 checks passed
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

US + core hub merge audit, core#561 at 5a69ca8d835484fc582a6c638c231da7c9af2001 (squash): supplied-input records now follow each storage tier, and compound-period disk filenames are portable to Windows.

@MaxGhenis
MaxGhenis deleted the fix-user-input-keys-drift branch October 9, 2026 05:43
MaxGhenis added a commit that referenced this pull request Oct 9, 2026
…ger period (#581)

* Count only inputs as already set when a set_input helper splits a longer period

set_input_divide_by_period and set_input_dispatch_by_period treated any value
stored for a sub-period as already set, including values the simulation had
calculated (a cached default or formula result). The same annual input then
gave different months depending on what was calculated first: a divided
input skipped calculated months and shared the rest among the others, or
failed as inconsistent after the year had been read; a dispatched input
reused a calculated month for every later month.

The helpers now read the simulation's record of inputs (_user_input_keys):
a recorded sub-period keeps its input (and, for dispatch, passes it on to
the later sub-periods, as before), and a calculated one is replaced. After
storing, they drop the variable's calculated values over overlapping periods
(the sums calculate_add caches, the twelfths calculate_divide caches) under
the input's branch and the branches it reads through, record what they store
as inputs, and evict calculate's fast cache for those periods.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Evict calculate's fast cache for the periods a set_input helper drops

calculate does not put sums or twelfths in its fast cache today, so this is
defensive: a value held there for a period whose stored value the helper
drops goes with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Restore dumped values as inputs; test nested branches and rolling years

A dump does not record which values were inputs, and restore_simulation
stored every value with put_in_cache, so a restored simulation had an empty
input record and the helpers replaced restored inputs (review finding 1).
Restore now stores each value as a recorded input, the rule #576 applies to
dumps without an inputs.txt.

Tests: a restored month keeps its value under a yearly input (divide and
dispatch); an input on a nested branch drops the sum its parent branch
calculated; an input stored for twelve months from March is kept. The
order-independence property now creates up to two nested branches, with
calculations at each level.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep the property test off disk file names Windows rejects

On disk, a value calculated for a period like year:2013:2 is stored in a
file named after the period, and Windows rejects ":" in file names
(policyengine-core#526). The Windows CI jobs failed on such a read in the
on-disk examples. In disk mode the property now skips those periods.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Record which dumped values were inputs; forget an input a calculation replaces

Review round 2:

1. Restoring every dumped value as an input froze calculated values through
   a later reform. The dumper now writes, next to each variable's arrays,
   the periods whose value was an input (inputs.txt), and restore records
   exactly those; a dump without the file restores every value as an input.
   This is policyengine-core#576's dumper change, taken byte for byte so the
   two PRs merge without conflict.

2. A value stored by put_in_cache over an input (say a calculate_add sum over
   an input set for twelve months) left the input's record entry in place,
   so the helpers and apply_reform kept treating the calculated value as an
   input. Holder._set now drops the entry, in both of its forms for twelve
   months from the first of a month, when it stores a value outside
   set_input.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Test a calculated value replacing an input with put_in_cache, not calculate_add

With policyengine-core#571, calculate_add no longer caches a sum over an input
a plain read finds, so the input is kept and the test's premise (master's
calculate_add overwriting it) did not hold. Storing the calculated value with
put_in_cache replaces the input with or without #571.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drop the record cleanup on calculated writes; evict the fast cache only for read branches

Review round 3:

- Holder._set no longer drops a record entry when a calculated value is
  stored over an input. A calculate_add at a variable's own period stores
  the unchanged input and lost its record; a clone, which shares its
  source's record until #561, deleted the source's entries; and ETERNITY
  variables keep entries under several periods. The case the cleanup was
  for, calculate_add caching a sum over an input, is what #571 stops.
- Master now gives a storage that shares nothing a frozenset for _shared
  (#578). The drop step deleted from it directly; it now leaves that to
  InMemoryStorage._stop_sharing_dropped_keys.
- The helpers evicted calculate's fast cache for a sub-period stored under
  any branch name, including one the simulation does not read. They now
  evict only for branches it reads, as #576's holder-write property
  requires.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

_user_input_keys drifts from stored values after delete_arrays and clone

1 participant