Four independent reviewers swept the #123635 diff. These are the findings that
changed code; each was verified before applying, not taken on report.
Register the recorded key instead of relying on the underscore sweep. The
staged dict becomes the turn's live user message, so _submit_row_session_id
rides every replay of that row. turn_context.py pops PERSISTENCE_ONLY_MESSAGE_FIELDS
from every outgoing copy and its comment says only chat-completions strips
underscore keys — an unregistered key survives to the transport, so a strict
OpenAI-compatible backend could 400 on it. Verified by execution: the registry
pop leaves the key in place and the transport sweeper is what removes it. The
membership is the contract; the underscore is the accident.
Gate each write on the key it writes to. _write_submit_user_row checked
session_key and appended to _submit_row_target_key(session); the two can
disagree after a rotation, so the guard described a write that was not the one
performed. The dead `key` local is gone.
Drop the isinstance/Any guard on _submit_row_owner_key. All three callers
narrow to a dict and check _row_id immediately before; the annotation existed
only to permit the non-dict the guard then rejected.
Log a failed model-switch marker persist at warning, not debug. Filing the
pivot in the live session means the write can now hit CompressionSessionClosedError
on a closed parent, which the old session_key target could not. At debug a
model switch silently loses its durable notice and nothing reaches errors.log —
the sibling persist one screen up already logs at warning.
Test-file: replace the E731 lambda with a named helper, and move _rotated_session
above its first use.
Verified: 7/7 in the regression file, 2113/2113 across the tui_gateway siblings
plus tests/hermes_state, 79/79 in the message-metadata/turn-context suites.