Preserve unread session state fields when marshaling after a read - #1057
Conversation
stateValue.MarshalJSON preferred the cached typed value over the original raw JSON. readInto caches a typed copy the first time a key is read, and that copy can be a narrower/partial view of the stored object. So a Session that was deserialized from persisted JSON, then read with a partial struct, then re-serialized would silently drop the fields that were never read - corrupting the persisted state on the next save, contrary to Session's documented encoding/json round-trip contract. Prefer the raw JSON when present (the authoritative serialized form for a deserialized value); values created via Set have raw == nil and still marshal losslessly from cached.
There was a problem hiding this comment.
🟢 Approval recommended
The fix includes regression coverage and no blocking issues were identified.
Pull request overview
Preserves unread session state fields when values are read and re-marshaled.
Changes:
- Prefer original raw JSON when available.
- Add regression coverage for partial reads.
File summaries
| File | Summary |
|---|---|
agent/value.go |
Preserves original raw state JSON during marshaling. |
agent/session_test.go |
Tests unread-field preservation after Get. |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Scope: internal-only Changed Go contract: None. Upstream evidence reviewed:
Result: aligned. The fix (preferring
|
Problem
stateValue.MarshalJSON(agent/value.go) preferred the cached typed value over the original raw JSON:readIntocaches a typed copy the first time a key is read — and that copy can be a narrower/partial view of the stored object (a forward/backward-compat partial read, or a different component's view). So this sequence loses data:Sessionis deserialized from persisted JSON where keykholds{"A":1,"B":2}.session.Get("k", &struct{ A int }{})— caching the typed value{A:1}.Result:
{"k":{"A":1}}— fieldBis silently dropped. Without the interveningGet, marshaling returns the raw{"A":1,"B":2}intact. So a read corrupts the persisted state on the next save, contrary toSession's documented contract that it "can be serialized and deserialized directly with encoding/json … saved in a persistent store."Fix
Prefer the raw JSON when present — it is the authoritative serialized form for a deserialized value and preserves unread fields. Values created via
Sethaveraw == niland still marshal losslessly fromcached.Test
TestSession_MarshalAfterGet_PreservesUnreadFieldsdeserializes a session, reads a key with a partial struct, re-marshals, and asserts the output equals the original. Fails before the fix (Bdropped), passes after.