Repository navigation
fix(sleep): read and write state.json as UTF-8 - #302
PerryLink (PerryLink) wants to merge 2 commits into
Conversation
|
Explicit UTF-8 for newly written state is useful, but the October 6 review of A state file successfully produced and read by the existing writer under GBK or cp1252 becomes unreadable by the new UTF-8-only loader on that same simulated locale. The existing broad exception handler then returns fresh state, and the next save overwrites the original memory/history/cursors. The baseline producer was used to verify the legacy fixture bytes, not merely a hand-written corrupt file. The positive state slice passed 13 tests. The independent upgrade slice had 4 failures and 1 UTF-8 control pass; the original-main compatibility controls passed. These are Linux simulations of the target module's implicit codec, not native Windows results. No user data was accessed. Please preserve UTF-8 for new writes while adding an explicit legacy migration/compatibility path, or at minimum fail with an actionable diagnostic and prevent overwriting an undecodable existing state. Add a load-then-save upgrade regression that preserves prior fields, including unknown fields. This is a data migration, not a safe two-line change to merge first. Related #304 has the analogous configuration-upgrade concern. |
The previous revision read and wrote state.json as UTF-8. That is right for new files, but not for existing ones: a state file written by an earlier release on a GBK or cp1252 box holds those bytes, so the UTF-8 read raised and the bare `except Exception: pass` turned the failure into a fresh state. The next save() then wrote over the original, and the night counter, the harvest cursors, the history and the cross-night memory were gone with no warning. Read the bytes and decode as UTF-8 first; on a UnicodeDecodeError fall back to the locale codec -- the one the old writer used on the machine that produced the file -- and let the next save() rewrite it as UTF-8, so the migration completes on the first write. A file that decodes under neither is now reported as StateFileError rather than replaced. The message names the path and says the file was left untouched. Continuing to return a fresh state is what makes the loss silent, because a reset state is indistinguishable from a first run. Tests: a legacy GBK fixture carrying an unknown field is loaded, saved and read back as UTF-8 with every field intact; an unreadable file raises while its bytes stay on disk. Both fail on the previous revision -- verified by running them against it -- and the locale codec is injected so the migration test is meaningful on a UTF-8 runner rather than passing by accident. test_sleep_state.py: 5 passed. tests/test_model_change_warning.py and tests/test_split_hardening_2x3.py are unchanged (4 failures there are a missing optional `llm_client` import, present before this change too).
|
Fixed in What the previous revision did. I reproduced the loss directly, with a state file written by the old locale-codec writer: The file on disk was replaced by a fresh one. The change. A file that decodes under neither is no longer replaced. Tests.
Thanks for testing this with the real producer rather than a hand-written corrupt file — a hand-written fixture would have missed the case entirely. |
|
@microsoft-github-policy-service agree |
skillopt_sleep/state.pyreads and writesstate.jsonwith no explicit encoding, whilesave()deliberately passesensure_ascii=False. The file therefore lands in the platform's locale charset, and two things follow -- the second is the serious one.1. On a locale that cannot represent the text,
save()raises. On a GBK machine:2. Even when it does not raise, the bytes are not UTF-8, and
load()swallows the failure.load()wraps its decode in a bareexcept Exception: pass, so any reader whose locale codec is UTF-8 -- a container, CI, WSL,PYTHONUTF8=1, or simply another machine -- gets a decode error, discards it, and returns a fresh state:Night counter, harvest cursors, history and the cross-night memory all disappear without a warning. The file is readable only by the machine that wrote it.
This is the defect
#124fixed inskillopt/config.py-- "read YAML config files as UTF-8", merged 2026-07-12, one line:state.pywas simply missed by that sweep. The change here is the same two lines:Tests
Two tests in
tests/test_sleep_state.py, and the second exists because the first is not sufficient:test_state_file_round_trips_non_ascii_as_utf8-- saves a CJK lesson and a CJK project path, asserts the bytes on disk decode as UTF-8, and asserts the round-trip preserves both.test_state_io_does_not_fall_back_to_the_locale_codec-- repeats the round-trip in a subprocess under-X warn_default_encoding -W error::EncodingWarning.The behavioural test cannot fail on a UTF-8 CI runner, because there the locale codec is UTF-8. On its own it would ship a regression test that never goes red. The warning-to-error guard makes CPython's own
EncodingWarninga failure on every platform, which is what actually pins the fix.Before (source fix reverted, tests kept):
After: