Repository navigation
fix(sleep): read and write state.json as UTF-8 - #302
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 |
|
The original encoding-upgrade regression is fixed at One gap remains in the new unreadable-state contract: a JSON list, null or scalar is returned as an empty mapping, so load followed by save can replace it with fresh state. The three independent non-object cases fail on this head; this behavior is inherited from main, not a newly introduced codec regression. Please reject non-object state with the same actionable StateFileError and preserve its bytes, while keeping the working migration tests. I am holding this migration change for that narrow contract completion, and coordinating it with #320 rather than merging overlapping reader implementations blindly. |
|
Pushed
if not isinstance(data, dict):
raise StateFileError(
f"{path} holds a JSON {type(data).__name__}, not a JSON object, so it cannot be "
f"used as state. It has been left untouched. ..."
)Tests: Locally, a standalone harness loading the real |
|
Merged as Re-reviewed The actual merged main The earlier non-object behavior was inherited from main and is no longer a blocker on this head. The split-exposure defaults already introduced by #272 are expected schema additions, not lost state. Thank you for the focused follow-up. |
microsoft#302 now provides the state reader with path-bearing diagnostics, so drop this branch's separate reader and its duplicate legacy-codec tests. The lock and the unique-tempfile save remain.
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: