fix(state): gateway /retry re-sends a multi-part carrier's stored bytes, not a "\n"-joined view

`rewind_user_turn` called `retryable_user_text` for validation only and returned
`flatten_message_text(...)` (`"\n".join` of the parts) as `live_text`, which the
gateway used as the re-sent prompt: `[{text:"a"},{text:"b"}]` went back to the
model as `"a\nb"` where origin/main sent `"ab"`. When `require_retryable` the
outcome now carries the lossless `"".join` (wire bytes == stored bytes); the
display flattening stays for /undo prefill.

Review follow-up on #109610.
This commit is contained in:
teknium1
2026-09-12 23:45:06 -07:00
committed by Teknium
parent c0be0e0826
commit e3281fe249
2 changed files with 37 additions and 4 deletions

View File

@@ -21,7 +21,7 @@ class RewindTargetUnavailableError(ValueError):
class RewindOutcome:
prefix: List[Dict[str, Any]] # history to install: the warm prefix when ``warm_history`` was given, else durable
live_view: Dict[str, Any] # canonical live projection of the rewound turn (prefill / retry source)
live_text: str
live_text: str # lossless retry text when ``require_retryable``, else the display flattening (prefill)
rewound_count: int
turns_undone: int
@@ -85,8 +85,9 @@ class SessionRewindMixin:
prefix, warm_live_view = history_before_user_originated_turn(warm, warm_user[user_ordinal])
if _comparison_content(live_view) != _comparison_content(warm_live_view):
raise RuntimeError(_HISTORY_CHANGED)
if require_retryable:
retryable_user_text(live_view.get("content"))
# Retry re-sends the stored bytes: ``"".join`` of the text parts, never the "\n"-joined display
# flattening (wire bytes == stored bytes; ``"ab"`` must not come back as ``"a\nb"``).
live_text = retryable_user_text(live_view.get("content")) if require_retryable else None
target_row_id = target.get("_row_id")
if not isinstance(target_row_id, int):
raise RuntimeError("rewind target has no durable row identity")
@@ -113,5 +114,6 @@ class SessionRewindMixin:
if isinstance(row_id := durable_message.get("_row_id"), int):
warm["_row_id"] = row_id
return RewindOutcome(
prefix=prefix, live_view=live_view, live_text=flatten_message_text(live_view.get("content")),
prefix=prefix, live_view=live_view,
live_text=live_text if live_text is not None else flatten_message_text(live_view.get("content")),
rewound_count=int(result.get("rewound_count", 0)), turns_undone=len(durable_user) - user_ordinal)