Fix hydration override mutation dependents
Fixes #7571.
#7568 avoids recomputing cached vars for unchanged browser storage, but its fields-only replay also drops cached dependents invalidated by assignments inside a get_delta() override.
Mark cached storage dependents for delivery without invalidating their snapshot caches, then pass the storage values through one public get_delta() traversal. Real assignments invalidate caches normally. Uncached values stay in the normal event delta, avoiding an extra evaluation during hydration.
This retains output-only overrides, field/computed filters, shared-state cleanup, and the single cached evaluation for unchanged storage. The original two-pass implementation was replaced after review reproduced duplicate override side effects and reintroduced omitted fields; both now have regression tests. No intermediate-delta merge or deferred recording machinery remains.
Contract
This restores the pre-#7568 traversal/return semantics. An override assigning after super().get_delta() must update any already-collected fields it wants changed in its returned delta. Synchronous values collected earlier in the traversal are not rewritten afterward. Async cached sibling dependents resolve after the traversal and observe the corrected storage value, as in the issue reproduction.
Two broader cases remain outside this regression fix: assigning an unrelated field after super().get_delta() can leave its already-visited/absent sibling dependent stale; explicitly injecting a placeholder for an unchanged uncached value can leave its old delivery record intact. Targeted reproductions of both fail on pre-#7568 base 2fc4a9caa5ce125acd9ce1f2e13cf5910ca5f8d8 as well (2 failed, 3 controls passed). This PR does not claim to repair those inherited behaviors.
Verification
154 added cases pass, including:
- 144 assignment/storage cases: before/after-super assignments, explicit return updates, LocalStorage/SessionStorage/Cookie, hashes and on-load on/off, in-process/mocked Redis
- output-only and mixed assignment filters
- duplicate side effects and omission of newly assigned fields/cached computed values
- shared cleanup and failed correction emit
The exact minimal reproduction in #7571 fails on merged main and passes with this revision. All 144 assignment cases plus the two review regressions match pre-#7568 observable behavior (146/146); cached-evaluation counts are checked separately because avoiding those extra evaluations is the intentional improvement. Existing cached-once and uncached-no-extra evaluation controls pass.
Full Python 3.14.8 unit-suite rerun against unmodified 7ccaa4e213fa1f67374002cb28709272866c3d53:
- Revised patch: 13,059 passed, 42 failed, 67 skipped
- Clean baseline: 12,904 passed, 43 failed, 67 skipped
- All 42 patched failures also occur on baseline. The extra baseline-only failure is its checkout-path fixture under the shared editable environment. No new failing test node IDs; full suite/coverage is not claimed green.
- Focused processor/state/shared/delta suites: 589 passed and one baseline tracing failure.
Full Ruff lint/format checks and Pyright pass (0 errors, 0 warnings). Published tree matches the tested revision. No news fragment: #7568 has not shipped in any release, so there is no user-facing change to record (skip-changelog).
Redis coverage uses the repository's mocked fixture. No local browser end-to-end test was run. CI results should be checked on the latest head.
Update: merged with main
main was merged into the branch; the only conflict was in test_base_state_processor.py, where both sides appended tests, and both sets are kept. On the merged head the processor, istate, and test_state suites pass (850 tests), and Ruff and Pyright are clean. The two Greptile P1 findings on the latest revision (a dependent of a field assigned after super().get_delta(); a placeholder injected for an unchanged uncached var) were reproduced and fail identically on the pre-#7568 base 8b48dc01c, so they are inherited get_delta override behavior rather than regressions of this change; details are in the thread replies.
Update: review round
The hydration replay now marks dependents through _mark_dirty_computed_vars(..., invalidate=False) instead of a second copy of the dependency walk, so one walk serves both cache invalidation and delivery-only marking. Benchmarked against main: the repo's test_state_delta benchmarks and a microbenchmark of _mark_dirty_computed_vars show no difference beyond run-to-run noise, with roughly 20 ns on the no-dependents path for the extra parameter and check (numbers in the review thread). The shared-cleanup test moved to tests/units/test_state.py and asserts the override's correction took effect. The news fragment is removed, as above.
🤖 Generated with Claude Code