patch: recover interrupted streams after delivered output (#1986)
Some checks are pending
Post merge / Warm dependency cache (Windows) (push) Waiting to run
Post merge / Warm dependency cache (macOS) (push) Waiting to run
Post merge / Warm dependency cache (Linux) (push) Waiting to run
Post merge / Publish to PyPI (push) Waiting to run

## Why

The hidden-stream retries in #1974 stop being sufficient once the
harness receives output. A later connection failure must preserve that
output and the completed calls protected by #1973 while recovering the
unfinished turn.

## How

- Continue with the same provider and model using released text and
readable reasoning within the existing attempt budget. The harness owns
conversation history.
- Give one public response owner responsibility for projection,
tool-group release, continuation mapping, publication and failure
finalization. Inspect native completion snapshots before mapping so
completed calls survive recovery.
- Permit request corrections only before that physical attempt publishes
output. Retain accepted corrections across later interruptions by
replacing only the private continuation turns in the current request.
Preserve released calls and finish complete calls as a tool handoff
without another generation.
- Reconcile exact replay across text parts and message items while
preserving source identities. Keep streamed text, completion snapshots
and later continuation history consistent, with one public response
identity and increasing sequence numbers.
- Handle nullable reasoning content and distinguish FCC replay records
for readable reasoning from genuine native signatures or encryption.
Refresh readable records at synthetic closure so later harness requests
retain all delivered reasoning.
- Count only real published content as continuation progress. Keep
compatibility padding limited to initial generation, and prevent
synthetic closures from turning an empty or echo-only continuation into
success.
- Bound recovery retention. After retained state is exhausted, preserve
delivered frames and terminate later failures with the existing
`response.failed` envelope, empty output, null usage, and an explanation
that the complete final snapshot is unavailable.
- Rebuild retained failure snapshots for pending as well as active
recovery. A timeout before the first continuation event preserves the
partial answer without requiring successful item closures.
- Preserve provider error categories and execution cleanup, including
when failed snapshots contain native content. Inspect those snapshots
for recovery restrictions without replacing their original error. Apply
annotation restrictions to part snapshots as well as dedicated and item
events.
- Keep delivered interruptions in structured-output requests and public
Responses text with token probabilities on the failure path. Block
reconstruction before tool handoff or replay trimming, while preserving
healthy output and hidden retries.

Accepted tradeoffs: continuation is best effort, and exact overlap
removal can remove intentional repetition. Ambiguous replay can delay
frames within a fixed retention bound. After recovery state is
discarded, a failed response cannot reconstruct the whole answer;
consumers retain prior stream events. The existing matcher's performance
optimization for exceptionally long partial replays remains explicitly
excluded. Chat's pre-existing error adapter can still close a partial
JSON fragment before reporting failure, causing typed SDK parsing to
raise; that projection limitation was verified on the base branch.

Validation: repository suppression, Ruff, Ty and pytest checks passed
locally (7,933 passed, 13 skipped). Official Anthropic and OpenAI SDK
consumers passed 107 cases. Coverage includes text/reasoning/tools,
replay across parts, retry visibility, tool-result follow-up, retention
failures, synthetic closure progress, nullable reasoning, later history
replay, corrections across repeated interruptions, empty continuations,
structured-output eligibility, token metadata, native error categories,
execution failure accounting, timeout before continuation output,
pending handoff failure, cancellation and healthy large responses.




<!-- greptile_comment -->

<!-- greptile_summary -->

<h2><a
href="https://app.greptile.com/api/retrigger?id=74179686"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/RetriggerDark.svg?v=2"><source
media="(prefers-color-scheme: light)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/Retrigger.svg?v=2"><img
alt="Retrigger"
src="https://greptile-static-assets.s3.amazonaws.com/badges/Retrigger.svg?v=2"
align="right"></picture></a>Confidence Score: 5/5</h2>

<!-- greptile-risk -->

No blocking findings remain. The outstanding overlap-search delay is
non-blocking.

<h2><a
href="https://app.greptile.com/api/ide/codex?prompt=IMPORTANT%3A%20Work%20in%20the%20repository%20%22alishahryar1%2Ffree-claude-code%22%20on%20the%20existing%20branch%20%22ali%2Frecover-delivered-streams%22.%20Checkout%20that%20branch%20%E2%80%94%20do%20NOT%20create%20a%20new%20branch%20or%20open%20a%20new%20PR.%20Push%20your%20changes%20to%20%22ali%2Frecover-delivered-streams%22.%0A%0A%23%23%23%20Issue%201%0Asrc%2Ffree_claude_code%2Fcore%2Fcontinuation_stream.py%3A239-243%0AWhen%20a%20continuation%20begins%20with%20a%20long%20substring%20of%20delivered%20text%20that%20is%20not%20its%20suffix%2C%20this%20loop%20tests%20every%20possible%20overlap%20before%20publishing%20the%20next%20delta.%20A%20524%2C288-character%20prefix%20delayed%20that%20delta%20by%203.1%20seconds%2C%20making%20a%20valid%20stream%20appear%20stalled.%20This%20is%20a%20non-blocking%20responsiveness%20concern%3B%20a%20linear-time%20overlap%20search%20would%20avoid%20the%20delay.%0A%0ANote%3A%20If%20this%20suggestion%20doesn't%20match%20your%20team's%20coding%20style%2C%20reply%20to%20this%20and%20let%20me%20know.%20I'll%20remember%20it%20for%20next%20time!%0A%0A---%0A%0AFor%20each%20issue%20above%2C%20determine%20whether%20it%20is%20valid%20and%20should%20be%20fixed.%20If%20so%2C%20fix%20it%20directly.&repo=alishahryar1%2Ffree-claude-code&pr=1986&platform=github"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCodexDark.svg?v=7"><source
media="(prefers-color-scheme: light)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCodex.svg?v=7"><img
alt="Fix All in Codex"
src="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCodex.svg?v=7"
align="right"></picture></a>Findings</h2>

1. <img alt="P2"
src="https://greptile-static-assets.s3.amazonaws.com/badges/p2.svg?v=9"
align="top">&nbsp;**Overlap search delays streaming** <a
href="https://github.com/Alishahryar1/free-claude-code/pull/1986#discussion_r4175258337">▶</a>

<details open><summary>Summary</summary>

The PR adds recovery for provider streams interrupted after output has
reached the client. The latest change preserves retained public output
when a pending continuation fails before emitting another event. No new
findings were identified.
</details>

<!-- greptile_confidence_score:5 -->

<sub>Reviews (9) · Last reviewed commit: ["fix: retain output when
pending
recovery..."](cf093468ee)</sub>

<!-- /greptile_comment -->
This commit is contained in:
Ali Khokhar 2026-10-04 12:50:17 -07:00 • committed by GitHub
parent f29cc304bf
commit c61d7bf44f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 6103 additions and 312 deletions

View file

@ -0,0 +1,47 @@
"""Project classifier content within one public attempt."""
from free_claude_code.core.anthropic.stream_contracts import parse_sse_text
from free_claude_code.core.anthropic.streaming import format_sse_event
class ClassifierProjection:
"""Project one attempt's blocks without interpreting classifier text."""
def __init__(self) -> None:
self._indices: dict[int, int | None] = {}
self._next_index = 0
def feed(self, frame: str) -> str | None:
events = parse_sse_text(frame)
if not events:
return frame
event = events[0]
payload = event.data
kind = payload.get("type", event.event)
if kind not in {
"content_block_start",
"content_block_delta",
"content_block_stop",
}:
return frame
index = payload.get("index")
if not isinstance(index, int):
return frame
if kind == "content_block_start":
block = payload.get("content_block")
if isinstance(block, dict) and block.get("type") in {
"thinking",
"redacted_thinking",
}:
self._indices[index] = None
else:
self._indices[index] = self._next_index
self._next_index += 1
if index not in self._indices:
return frame
visible_index = self._indices[index]
if kind == "content_block_stop":
del self._indices[index]
if visible_index is None:
return None
return format_sse_event(event.event, {**payload, "index": visible_index})

View file

@ -3,76 +3,27 @@
import sys
from collections.abc import AsyncIterator
from free_claude_code.core.anthropic.stream_contracts import SSEEvent
from free_claude_code.core.anthropic.streaming import format_sse_event
from free_claude_code.core.anthropic.streaming.decoder import AnthropicSSEDecoder
from free_claude_code.core.stream_delivery import current_stream_delivery
from free_claude_code.core.trace import close_stream_input
from ..classifier_projection import ClassifierProjection
async def classifier_response(source: AsyncIterator[str]) -> AsyncIterator[str]:
"""Project block lifecycles without interpreting any classifier text."""
"""Apply the same projection to private, nonstreaming aggregation."""
decoder = AnthropicSSEDecoder()
indices: dict[int, int | None] = {}
next_index = 0
delivery = current_stream_delivery()
revision = delivery.attempt_revision if delivery else 0
def synchronize() -> None:
nonlocal decoder, next_index, revision
if delivery is None or revision == delivery.attempt_revision:
return
revision = delivery.attempt_revision
decoder = AnthropicSSEDecoder()
indices.clear()
next_index = 0
def project(event: SSEEvent) -> str | None:
nonlocal next_index
payload = event.data
kind = payload.get("type", event.event)
if kind not in {
"content_block_start",
"content_block_delta",
"content_block_stop",
}:
return event.raw + "\n\n"
index = payload.get("index")
if not isinstance(index, int):
return event.raw + "\n\n"
if kind == "content_block_start":
block = payload.get("content_block")
if isinstance(block, dict) and block.get("type") in {
"thinking",
"redacted_thinking",
}:
indices[index] = None
else:
indices[index] = next_index
next_index += 1
if index not in indices:
return event.raw + "\n\n"
visible_index = indices[index]
if kind == "content_block_stop":
del indices[index]
if visible_index is None:
return None
return format_sse_event(event.event, {**payload, "index": visible_index})
projection = ClassifierProjection()
try:
async for chunk in source:
synchronize()
for event in decoder.feed(chunk):
projected = project(event)
for frame in decoder.feed_frames(chunk):
projected = projection.feed(frame)
if projected is not None:
yield projected
synchronize()
for event in decoder.finish():
projected = project(event)
for frame in decoder.finish_frames():
projected = projection.feed(frame)
if projected is not None:
yield projected
finally:
synchronize()
await close_stream_input(
source,
owner="classifier_response",

View file

@ -157,14 +157,11 @@ class MessagesHandler:
request_id=request_id,
)
)
if routed.reasoning.control is ReasoningControl.PREFER_OFF and isinstance(
result, _MessagesStreamResult
):
result = _MessagesStreamResult(classifier_response(result.body))
return await self._to_public_response(
result,
stream=request_data.stream,
request_id=request_id,
hide_reasoning=routed.reasoning.control is ReasoningControl.PREFER_OFF,
)
except ApplicationError:
raise
@ -184,6 +181,7 @@ class MessagesHandler:
*,
stream: bool,
request_id: str,
hide_reasoning: bool = False,
) -> object:
if isinstance(result, _MessagesCompleteResult):
return result.response
@ -193,7 +191,7 @@ class MessagesHandler:
# serving that raw here breaks the client SDK's response parse.
try:
message, error, _complete = await aggregate_anthropic_sse_to_message(
result.body
classifier_response(result.body) if hide_reasoning else result.body
)
except GeneratorExit:
raise
@ -244,6 +242,7 @@ class MessagesHandler:
exc, request_id=request_id
),
request_id=request_id,
hide_reasoning=hide_reasoning,
)
def _pre_start_error_response(

View file

@ -36,8 +36,7 @@ from free_claude_code.core.request_outcomes import (
from free_claude_code.core.stream_delivery import StreamDeliveryState
from free_claude_code.core.trace import close_stream_input, trace_event
from .stream_delivery import DeliveryObservedStream, PublicStreamEnvelope
from .tool_call_buffer import ToolCallBufferedStream
from .stream_delivery import DeliveryObservedStream, PublicResponseStream
TERMINAL_EXECUTION_ERROR_HEADERS = {"x-should-retry": "false"}
@ -238,9 +237,9 @@ async def _first_chunk_streaming_response(
pre_start_error_response: PreStartErrorResponse,
terminal_frame: TerminalFrameEmitter | None,
terminal_failure_observer: TerminalFailureObserver | None,
hide_reasoning: bool = False,
) -> Response:
state = StreamDeliveryState()
envelope = PublicStreamEnvelope(state, wire_api=wire_api)
body = DeliveryObservedStream(body, state)
try:
first_chunk = await anext(body)
@ -262,16 +261,14 @@ async def _first_chunk_streaming_response(
return pre_start_error_response(exc)
return ManagedStreamingResponse(
ToolCallBufferedStream(
_PrefetchedStream(
first_chunk,
body,
terminal_frame=terminal_frame,
terminal_failure_observer=terminal_failure_observer,
envelope=envelope,
),
PublicResponseStream(
_PrefetchedStream(first_chunk, body),
state,
wire_api=wire_api,
envelope=envelope,
first_chunk=first_chunk,
terminal_frame=terminal_frame,
terminal_failure_observer=terminal_failure_observer,
hide_reasoning=hide_reasoning,
),
media_type="text/event-stream",
headers=dict(headers),
@ -296,92 +293,46 @@ async def _close_pre_start_body(
class _PrefetchedStream(AsyncIterator[str]):
"""Replay one prefetched frame while retaining ownership of the tail."""
"""Replay the raw prefetched chunk and retain ownership of the tail."""
def __init__(
self,
first_chunk: str,
body: AsyncIterator[str],
*,
terminal_frame: TerminalFrameEmitter | None,
terminal_failure_observer: TerminalFailureObserver | None,
envelope: PublicStreamEnvelope,
) -> None:
def __init__(self, first_chunk: str, body: AsyncIterator[str]) -> None:
self._first_chunk: str | None = first_chunk
self._initial_chunk = first_chunk
self._latest_chunk = first_chunk
self._body = body
self._terminal_frame = terminal_frame
self._terminal_failure_observer = terminal_failure_observer
self._done = False
self._closed = False
self._envelope = envelope
self._revision = envelope.state.attempt_revision
def __aiter__(self) -> _PrefetchedStream:
return self
async def __anext__(self) -> str:
if self._closed or self._done:
if self._closed:
raise StopAsyncIteration
if self._first_chunk is not None:
first_chunk = self._first_chunk
self._first_chunk = None
first_chunk, self._first_chunk = self._first_chunk, None
return first_chunk
try:
chunk = await anext(self._body)
self._synchronize()
self._latest_chunk = chunk
return chunk
except StopAsyncIteration:
self._done = True
raise
except BaseExceptionGroup as exc:
return self._terminal_chunk(find_execution_failure(exc) or exc)
except Exception as exc:
return self._terminal_chunk(exc)
return await anext(self._body)
async def aclose(self) -> None:
if self._closed:
return
self._closed = True
self._done = True
close_error = await try_close_async_iterator(self._body)
if close_error is not None:
raise close_error
def _terminal_chunk(self, exc: BaseException) -> str:
self._synchronize()
terminal_frame = self._terminal_frame
if terminal_frame is None:
raise exc
self._done = True
if self._terminal_failure_observer is not None:
self._terminal_failure_observer(exc)
return terminal_frame(
self._envelope.start_frame or self._initial_chunk, self._latest_chunk, exc
)
def _synchronize(self) -> None:
if self._revision == self._envelope.state.attempt_revision:
return
self._revision = self._envelope.state.attempt_revision
self._envelope.synchronize()
self._initial_chunk = self._envelope.start_frame
self._latest_chunk = ""
async def anthropic_sse_streaming_response(
body: AsyncIterator[str],
*,
pre_start_error_response: PreStartErrorResponse,
request_id: str,
hide_reasoning: bool = False,
) -> Response:
"""Return a streaming response for Anthropic-style SSE streams."""
return await _first_chunk_streaming_response(
body,
wire_api="messages",
headers=ANTHROPIC_SSE_RESPONSE_HEADERS,
hide_reasoning=hide_reasoning,
pre_start_error_response=pre_start_error_response,
terminal_frame=_anthropic_terminal_frame,
terminal_failure_observer=lambda exc: _trace_anthropic_terminal_failure(

View file

@ -1,17 +1,26 @@
"""Observe public delivery and retain one response envelope across hidden retries."""
"""Own public projection, tool release, recovery mapping and wire termination."""
import re
from collections.abc import AsyncIterator, Mapping
from collections.abc import AsyncGenerator, AsyncIterator, Callable, Iterator
from copy import deepcopy
from typing import Any, Literal
import simplejson
from free_claude_code.core.anthropic.streaming.decoder import AnthropicSSEDecoder
from free_claude_code.core.async_iterators import try_close_async_iterator
from free_claude_code.core.continuation_stream import ContinuationStream
from free_claude_code.core.delivered_response import DeliveredResponse
from free_claude_code.core.failures import find_execution_failure
from free_claude_code.core.stream_delivery import (
StreamDeliveryState,
bind_stream_delivery,
)
from .classifier_projection import ClassifierProjection
from .stream_frames import frame_data, replace_frame_data
from .tool_call_buffer import ToolCallBuffer
class DeliveryObservedStream(AsyncIterator[str]):
"""Bind observation for each read/close, including prefetch in another task."""
@ -34,92 +43,233 @@ class DeliveryObservedStream(AsyncIterator[str]):
raise error
def frame_data(frame: str) -> dict[str, Any] | None:
"""Read only SSE data, retaining decimal precision for envelope edits."""
text = "\n".join(
line[5:].lstrip(" ")
for line in re.split(r"\r\n|\r|\n", frame)
if line.startswith("data:")
)
try:
value: object = simplejson.loads(text, use_decimal=True)
except ValueError:
return None
return value if isinstance(value, dict) else None
def replace_frame_data(frame: str, payload: Mapping[str, object]) -> str:
"""Replace data lines while preserving other SSE fields and line endings."""
lines = re.split(r"(\r\n|\r|\n)", frame)
replacement = "data: " + simplejson.dumps(
payload, use_decimal=True, ensure_ascii=False
)
parts: list[str] = []
replaced = False
for index in range(0, len(lines), 2):
line = lines[index]
ending = lines[index + 1] if index + 1 < len(lines) else ""
if line.startswith("data:"):
if replaced:
continue
line = replacement
replaced = True
parts.extend((line, ending))
return "".join(parts)
class PublicStreamEnvelope:
"""Own published metadata without reconstructing content or tool arguments."""
class PublicResponseStream(AsyncIterator[str]):
"""Publish one response while providers retain their physical request loops."""
def __init__(
self, state: StreamDeliveryState, *, wire_api: Literal["messages", "responses"]
self,
body: AsyncIterator[str],
state: StreamDeliveryState,
*,
wire_api: Literal["messages", "responses"],
first_chunk: str,
terminal_frame: Callable[[str, str, BaseException], str] | None,
terminal_failure_observer: Callable[[BaseException], None] | None,
hide_reasoning: bool = False,
) -> None:
self._body = body
self.state = state
self._wire_api = wire_api
self._revision = state.attempt_revision
self._hide_reasoning = hide_reasoning
self._decoder = AnthropicSSEDecoder()
self._projection = ClassifierProjection() if hide_reasoning else None
self._tools = ToolCallBuffer(wire_api)
self._delivered = DeliveredResponse(wire_api)
self._continuation = ContinuationStream(self._delivered)
state.bind_prefix(self._delivered.snapshot)
self._published_starts: set[str] = set()
self._seen_starts: set[str] = set()
self._replacement = False
self._public_id: str | None = None
self._created_at: object = None
self._sequence: int = -1
self._sequence = -1
self._offset: int | None = None
self._pending_starts: list[str] = []
self.start_frame = ""
self._initial_chunk = first_chunk
self._latest_chunk = ""
self._terminal_frame = terminal_frame
self._terminal_failure_observer = terminal_failure_observer
self._done = False
self._closed = False
self._events = self._iterate()
def synchronize(self) -> None:
if self._revision == self.state.attempt_revision:
def __aiter__(self) -> PublicResponseStream:
return self
async def __anext__(self) -> str:
if self._closed or self._done:
raise StopAsyncIteration
try:
while True:
frame, synthetic = await anext(self._events)
frame = self._finalize_frame(frame)
value = self._publish(frame, synthetic=synthetic)
if value is not None:
self._latest_chunk = value
return value
except StopAsyncIteration:
self._done = True
raise
except (Exception, BaseExceptionGroup) as exc:
self._done = True
self._pending_starts.clear()
self._tools = ToolCallBuffer(self._wire_api)
failure = find_execution_failure(exc) or exc
if self._terminal_frame is None:
raise
if self._terminal_failure_observer is not None:
self._terminal_failure_observer(failure)
frame = self._terminal_frame(
self.start_frame or self._initial_chunk, self._latest_chunk, failure
)
frame = self._finalize_frame(frame)
return self._publish(frame, synthetic=True) or frame
def _finalize_frame(self, frame: str) -> str:
payload = frame_data(frame)
if payload is None:
return frame
kind = payload.get("type")
if kind == "response.failed":
response = payload.get("response")
if not isinstance(response, dict):
return frame
# Failure snapshots may be based on the provider's empty start frame.
# Use only retained public evidence, independently of a failed mapper.
if self._delivered.retention_exhausted:
response["output"] = []
response["usage"] = None
error = response.get("error")
error = error if isinstance(error, dict) else {}
message = error.get("message") or "Provider response failed."
response["error"] = {
**error,
"message": f"{message}\n\nThe partial answer was already streamed. "
"A complete final snapshot is unavailable.",
}
elif self._continuation.active or self.state.transition is not None:
# A timeout can precede the first frame that activates recovery.
response["output"] = [
deepcopy(item) for _, item in sorted(self._delivered.items.items())
]
response["usage"] = self._delivered.usage(response["output"])
else:
return frame
elif not self._continuation.active or kind in {"error", "response.error"}:
return frame
else:
payload = self._continuation.finalize(payload)
return replace_frame_data(frame, payload)
async def _iterate(self) -> AsyncGenerator[tuple[str, bool]]:
while True:
try:
chunk = await anext(self._body)
except StopAsyncIteration:
for frame in self._synchronize():
yield frame, True
if self._continuation.handoff:
for payload in self._delivered.handoff_events():
for frame in self._prepare(self._format(payload)):
yield frame, True
else:
for frame in self._decoder.finish_frames():
for ready in self._process(frame):
yield ready, False
return
for frame in self._synchronize():
yield frame, True
for frame in self._decoder.feed_frames(chunk):
for ready in self._process(frame):
yield ready, False
def _synchronize(self) -> Iterator[str]:
transition = self.state.transition
if self._revision == self.state.attempt_revision and transition is None:
return
self._revision = self.state.attempt_revision
self._replacement = bool(self._published_starts)
self.state.transition = None
self._decoder = AnthropicSSEDecoder()
self._tools = ToolCallBuffer(self._wire_api)
self._projection = ClassifierProjection() if self._hide_reasoning else None
self._seen_starts.clear()
self._offset = None
self.discard_pending_starts()
def discard_pending_starts(self) -> None:
"""Discard metadata that no client has observed."""
self._pending_starts.clear()
self._offset = None
if transition is not None:
self._continuation.begin(handoff=transition == "handoff")
elif self._continuation.active:
self._continuation.restart_attempt()
self._replacement = (
bool(self._published_starts) and not self._continuation.active
)
for event in self._continuation.boundary():
yield self._format(event)
def _release_content(self, frame: str) -> str:
self.state.release_content()
def _process(self, frame: str) -> Iterator[str]:
if self._projection is not None:
projected = self._projection.feed(frame)
if projected is None:
return
frame = projected
for released in self._tools.feed(frame):
yield from self._prepare(released)
def _prepare(self, frame: str) -> Iterator[str]:
if not self._continuation.active:
yield frame
return
payload = frame_data(frame)
if payload is None:
yield frame
return
for item in self._continuation.prepare(payload):
yield (
replace_frame_data(frame, item)
if item.get("type") == payload.get("type")
else self._format(item)
)
@staticmethod
def _format(payload: dict[str, Any]) -> str:
return f"event: {payload['type']}\ndata: {simplejson.dumps(payload, use_decimal=True)}\n\n"
async def aclose(self) -> None:
if self._closed:
return
self._closed = self._done = True
await self._events.aclose()
self._pending_starts.clear()
self._decoder = AnthropicSSEDecoder()
self._tools = ToolCallBuffer(self._wire_api)
self.state.bind_prefix(None)
self._delivered = DeliveredResponse(self._wire_api)
self._continuation = ContinuationStream(self._delivered)
self.start_frame = self._initial_chunk = self._latest_chunk = ""
error = await try_close_async_iterator(self._body)
if error is not None:
raise error
def _release_content(self, frame: str, *, synthetic: bool) -> str:
self.state.release_content(synthetic=synthetic)
for start in self._pending_starts:
self._record(start, synthetic=synthetic)
self._record(frame, synthetic=synthetic)
if not self._pending_starts:
return frame
self.start_frame = self._pending_starts[0]
self._published_starts.add("message_start")
prefix = "".join(self._pending_starts)
self.discard_pending_starts()
self._pending_starts.clear()
return prefix + frame
def publish(self, frame: str) -> str | None:
def _record(self, frame: str, *, synthetic: bool) -> None:
payload = frame_data(frame)
if payload is not None:
self._delivered.observe(payload, count_progress=not synthetic)
elif any(line.startswith("data:") for line in frame.splitlines()):
self._delivered.unsafe_reason = "unknown_event"
def _publish(self, frame: str, *, synthetic: bool = False) -> str | None:
"""Normalize only a replacement envelope and mark actually released frames."""
self.synchronize()
payload = frame_data(frame)
if payload is None:
if any(
line.strip() and not line.startswith(":")
for line in re.split(r"\r\n|\r|\n", frame)
):
return self._release_content(frame)
return self._release_content(frame, synthetic=synthetic)
return frame
raw_kind = payload.get("type")
@ -130,7 +280,7 @@ class PublicStreamEnvelope:
self._pending_starts.append(frame)
return None
if kind == "error":
self.discard_pending_starts()
self._pending_starts.clear()
start = kind in {"message_start", "response.created", "response.in_progress"}
if start and metadata:
if kind not in self._seen_starts:
@ -181,7 +331,10 @@ class PublicStreamEnvelope:
number += self._offset
self._sequence = max(self._sequence, number)
frame = replace_frame_data(frame, payload) if changed else frame
return frame if metadata else self._release_content(frame)
if metadata:
self._record(frame, synthetic=synthetic)
return frame
return self._release_content(frame, synthetic=synthetic)
def _metadata(self, kind: str, payload: dict[str, Any]) -> bool:
if kind == "ping":

View file

@ -0,0 +1,41 @@
"""Read and replace SSE data without losing framing or decimal precision."""
import re
from collections.abc import Mapping
from typing import Any
import simplejson
def frame_data(frame: str) -> dict[str, Any] | None:
"""Read only SSE data, retaining decimal precision for envelope edits."""
text = "\n".join(
line[5:].lstrip(" ")
for line in re.split(r"\r\n|\r|\n", frame)
if line.startswith("data:")
)
try:
value: object = simplejson.loads(text, use_decimal=True)
except ValueError:
return None
return value if isinstance(value, dict) else None
def replace_frame_data(frame: str, payload: Mapping[str, object]) -> str:
"""Replace data lines while preserving other SSE fields and line endings."""
lines = re.split(r"(\r\n|\r|\n)", frame)
replacement = "data: " + simplejson.dumps(
payload, use_decimal=True, ensure_ascii=False
)
parts: list[str] = []
replaced = False
for index in range(0, len(lines), 2):
line = lines[index]
ending = lines[index + 1] if index + 1 < len(lines) else ""
if line.startswith("data:"):
if replaced:
continue
line = replacement
replaced = True
parts.extend((line, ending))
return "".join(parts)

View file

@ -1,15 +1,13 @@
"""Withhold incomplete client tool calls at the public SSE delivery boundary."""
from collections.abc import AsyncIterator, Mapping
from collections.abc import Mapping
from typing import Literal, cast
from free_claude_code.core.anthropic.stream_contracts import parse_sse_text
from free_claude_code.core.anthropic.streaming.decoder import AnthropicSSEDecoder
from free_claude_code.core.async_iterators import try_close_async_iterator
from free_claude_code.core.json_types import JsonValue
from free_claude_code.core.openai_responses import is_client_search
from .stream_delivery import PublicStreamEnvelope, frame_data, replace_frame_data
from .stream_frames import frame_data, replace_frame_data
type CallKey = int | str
@ -46,19 +44,11 @@ def _client_call(item: Mapping[str, object]) -> bool:
)
class ToolCallBufferedStream(AsyncIterator[str]):
"""Keep overlapping calls and intervening frames in their original order."""
class ToolCallBuffer:
"""Select complete groups in source order, without owning public delivery."""
def __init__(
self,
body: AsyncIterator[str],
*,
wire_api: Literal["messages", "responses"],
envelope: PublicStreamEnvelope | None = None,
) -> None:
self._body = body
def __init__(self, wire_api: Literal["messages", "responses"]) -> None:
self._wire_api = wire_api
self._decoder = AnthropicSSEDecoder()
self._frames: list[str] = []
self._calls: set[CallKey] = set()
self._pending: set[CallKey] = set()
@ -66,75 +56,8 @@ class ToolCallBufferedStream(AsyncIterator[str]):
self._discarded: set[CallKey] = set()
self._ids: dict[str, CallKey] = {}
self._indices: dict[int, CallKey] = {}
self._done = False
self._closed = False
self._envelope = envelope
self._revision = envelope.state.attempt_revision if envelope else 0
def __aiter__(self) -> ToolCallBufferedStream:
return self
async def __anext__(self) -> str:
while not self._closed and not self._done:
try:
chunk = await anext(self._body)
except StopAsyncIteration:
self._synchronize()
self._done = True
frames = self._decoder.finish_frames()
except BaseException:
self._synchronize()
self._discard_group()
if self._envelope is not None:
self._envelope.discard_pending_starts()
raise
else:
self._synchronize()
frames = self._decoder.feed_frames(chunk)
ready = [value for frame in frames for value in self._accept(frame)]
if self._envelope is not None:
ready = [
value
for frame in ready
if (value := self._envelope.publish(frame)) is not None
]
if self._done:
self._discard_group()
if self._envelope is not None:
self._envelope.discard_pending_starts()
if ready:
return "".join(ready)
raise StopAsyncIteration
def _synchronize(self) -> None:
envelope = self._envelope
if envelope is None or self._revision == envelope.state.attempt_revision:
return
self._revision = envelope.state.attempt_revision
self._discard_group()
self._decoder = AnthropicSSEDecoder()
self._ids.clear()
self._indices.clear()
self._delivered.clear()
self._discarded.clear()
async def aclose(self) -> None:
if self._closed:
return
self._closed = True
self._discard_group()
if self._envelope is not None:
self._envelope.discard_pending_starts()
self._decoder.finish_frames()
self._ids.clear()
self._indices.clear()
self._delivered.clear()
self._discarded.clear()
close_error = await try_close_async_iterator(self._body)
if close_error is not None:
raise close_error
def _accept(self, frame: str) -> list[str]:
def feed(self, frame: str) -> list[str]:
events = parse_sse_text(frame)
if not events:
return self._publish(frame)

View file

@ -0,0 +1,335 @@
"""Remove one logical replay prefix while preserving source parts and events."""
from copy import deepcopy
from dataclasses import dataclass
from typing import Any
import simplejson
from .delivered_response import RECOVERY_RETENTION_BYTES
from .failures import ExecutionFailure, FailureKind
type PartKey = tuple[int | str, int]
@dataclass(slots=True)
class _Part:
order: tuple[int, int]
text: str = ""
seen: int = 0
closed: bool = False
removed: int = 0
fixed: bool = False
@dataclass(slots=True)
class _Text:
key: PartKey
order: tuple[int, int]
value: dict[str, Any]
field: str
delta: bool = False
part: _Part | None = None
offset: int = 0
@dataclass(slots=True)
class _Frame:
data: dict[str, Any]
texts: list[_Text]
class ContinuationOverlap:
"""Own unpublished replay evidence, never the public response transcript."""
def __init__(self, expected: str) -> None:
self._expected = expected
self._pending = bool(expected)
self._parts: dict[PartKey, _Part] = {}
self._ids: dict[int, str] = {}
self._frames: list[_Frame] = []
self._frame_bytes = 0
self._frontier: tuple[int, int] | None = None
def feed(self, payload: dict[str, Any]) -> list[dict[str, Any]]:
data = deepcopy(payload)
block = data.get("content_block", {})
if (
data.get("type") == "content_block_start"
and block.get("type") == "text"
and block.get("text")
):
text, block["text"] = block["text"], ""
return [
*self._feed(data),
*self._feed(
{
"type": "content_block_delta",
"index": data["index"],
"delta": {"type": "text_delta", "text": text},
}
),
]
return self._feed(data)
def _feed(self, data: dict[str, Any]) -> list[dict[str, Any]]:
kind = data.get("type", "")
if kind in {"error", "response.error", "response.failed"}:
self._frames.clear()
self._frame_bytes = 0
return [data]
if not self._expected:
return self._render(_Frame(data, self._texts(data)))
texts = self._texts(data)
frame = _Frame(data, texts)
terminal = kind in {
"response.completed",
"response.incomplete",
"message_stop",
} or (
kind == "message_delta"
and data.get("delta", {}).get("stop_reason") is not None
)
if not self._pending:
if kind in {"response.completed", "response.incomplete"}:
self._check_terminal_order(texts)
for text in texts:
self._settled_text(text)
return self._render(frame)
if kind in {"response.completed", "response.incomplete"}:
supplied = {text.key for text in texts}
if any(
part.text and key not in supplied for key, part in self._parts.items()
):
self._inconsistent()
for text in texts:
part = self._parts.setdefault(text.key, _Part(text.order))
part.order = text.order
text.part = part
value = text.value.get(text.field, "")
text.offset = part.seen if text.delta else 0
if text.delta:
part.text = (part.text + value)[: len(self._expected) + 1]
part.seen += len(value)
else:
if not value.startswith(part.text):
self._inconsistent()
part.text = value[: len(self._expected) + 1]
part.seen = len(value)
if kind.endswith(".done"):
part.closed = True
self._close_parts(data, terminal=terminal)
ordered = sorted(self._parts.items(), key=lambda entry: entry[1].order)
candidate = ""
candidates: list[tuple[PartKey, _Part]] = []
for key, part in ordered:
candidates.append((key, part))
candidate += part.text[: len(self._expected) + 1 - len(candidate)]
if not part.closed or len(candidate) > len(self._expected):
break
if terminal or candidate not in self._expected:
self._resolve(candidate, candidates)
frames, self._frames = self._frames, []
self._frame_bytes = 0
self._check_size()
return [
event for ready in [*frames, frame] for event in self._render(ready)
]
if self._frames or any(text.value.get(text.field) for text in texts):
self._frames.append(frame)
self._frame_bytes += self._size(data)
self._check_size()
return []
self._check_size()
return self._render(frame)
def _identity(self, data: dict[str, Any], item: dict[str, Any]) -> int | str:
index = data.get("output_index", 0)
identity = item.get("id") or data.get("item_id") or self._ids.get(index, index)
if self._pending and isinstance(identity, str) and "output_index" in data:
self._ids[index] = identity
return identity
def _texts(self, data: dict[str, Any]) -> list[_Text]:
kind = data.get("type", "")
index = data.get("index", 0)
if kind == "content_block_start":
block = data.get("content_block", {})
return (
[_Text((index, 0), (index, 0), block, "text")]
if block.get("type") == "text"
else []
)
if kind == "content_block_delta":
delta = data.get("delta", {})
return (
[_Text((index, 0), (index, 0), delta, "text", delta=True)]
if delta.get("type") == "text_delta"
else []
)
if kind in {"response.completed", "response.incomplete"}:
texts = []
for position, item in enumerate(data.get("response", {}).get("output", [])):
texts.extend(self._item_texts(item, item.get("id", position), position))
return texts
item = data.get("item", {})
if kind in {"response.output_item.added", "response.output_item.done"}:
if item.get("type") != "message":
return []
identity = self._identity(data, item)
return self._item_texts(item, identity, data.get("output_index", 0))
if kind not in {
"response.content_part.added",
"response.content_part.done",
"response.output_text.delta",
"response.output_text.done",
}:
return []
position = data.get("content_index", 0)
key = (self._identity(data, {}), position)
order = (data.get("output_index", 0), position)
if kind in {"response.output_text.delta", "response.output_text.done"}:
delta = kind.endswith(".delta")
return [_Text(key, order, data, "delta" if delta else "text", delta=delta)]
part = data.get("part", {})
return (
[_Text(key, order, part, "text")]
if part.get("type") == "output_text"
else []
)
@staticmethod
def _item_texts(
item: dict[str, Any], identity: int | str, index: int
) -> list[_Text]:
if item.get("type") != "message":
return []
return [
_Text((identity, position), (index, position), part, "text")
for position, part in enumerate(item.get("content", []))
if part.get("type") == "output_text"
]
def _close_parts(self, data: dict[str, Any], *, terminal: bool) -> None:
kind = data.get("type")
identity = (
self._identity(data, data.get("item", {}))
if kind == "response.output_item.done"
and data.get("item", {}).get("type") == "message"
else data.get("index")
if kind == "content_block_stop"
else None
)
for key, part in self._parts.items():
if terminal or key[0] == identity:
part.closed = True
def _resolve(self, candidate: str, candidates: list[tuple[PartKey, _Part]]) -> None:
overlap = self._overlap_size(candidate)
remaining = len(candidate)
self._pending = False
self._parts = {}
for key, part in candidates:
if not part.text or not remaining:
continue
part.text = part.text[:remaining]
remaining -= len(part.text)
part.removed = min(overlap, len(part.text))
overlap -= part.removed
part.fixed = bool(remaining)
self._parts[key] = part
self._frontier = part.order
identities = {key[0] for key in self._parts}
self._ids = {
index: value for index, value in self._ids.items() if value in identities
}
def _overlap_size(self, candidate: str) -> int:
for size in range(min(len(self._expected), len(candidate)), 0, -1):
if self._expected.endswith(candidate[:size]):
return size
return 0
def _settled_text(self, text: _Text) -> None:
value = text.value.get(text.field, "")
part = self._parts.get(text.key)
if part is None:
if value and self._frontier is not None and text.order < self._frontier:
self._inconsistent()
return
text.part = part
if text.delta:
if value and part.fixed:
self._inconsistent()
text.offset = part.seen
part.seen += len(value)
else:
if not value.startswith(part.text) or (
part.fixed and len(value) != part.seen
):
self._inconsistent()
part.seen = max(part.seen, len(value))
def _check_terminal_order(self, texts: list[_Text]) -> None:
keys = list(self._parts)
observed = [text.key for text in texts if text.value.get(text.field)]
if observed[: len(keys)] != keys:
self._inconsistent()
# Terminal positions may have shifted when the tool gate removed an item.
for text in texts:
if text.key not in self._parts:
text.order = (
(self._frontier[0] + 1, 0) if self._frontier else text.order
)
@staticmethod
def _render(frame: _Frame) -> list[dict[str, Any]]:
for text in frame.texts:
if text.part is not None:
removed = (
max(0, text.part.removed - text.offset)
if text.delta
else text.part.removed
)
text.value[text.field] = text.value.get(text.field, "")[removed:]
if (
frame.texts
and all(text.delta for text in frame.texts)
and not any(text.value.get(text.field) for text in frame.texts)
):
return []
return [frame.data]
@staticmethod
def _size(value: object) -> int:
return len(
simplejson.dumps(value, use_decimal=True, ensure_ascii=False).encode(
"utf-8"
)
)
def _check_size(self) -> None:
retained = self._frame_bytes + self._size(self._ids)
retained += sum(
self._size((key, part.order, part.text, part.seen))
for key, part in self._parts.items()
)
if retained > RECOVERY_RETENTION_BYTES:
raise ExecutionFailure(
kind=FailureKind.UPSTREAM,
status_code=502,
message="The continuation replay exceeded the retained-data limit.",
retryable=False,
)
@staticmethod
def _inconsistent() -> None:
raise ExecutionFailure(
kind=FailureKind.UPSTREAM,
status_code=502,
message="The continuation changed an already observed answer prefix.",
retryable=False,
)

View file

@ -0,0 +1,157 @@
"""Map continuation events into an existing public response lifecycle."""
from copy import deepcopy
from typing import Any
from uuid import uuid4, uuid5
from .continuation_overlap import ContinuationOverlap
from .delivered_response import (
DeliveredResponse,
client_call,
responses_text_constraint,
)
from .failures import ExecutionFailure, FailureKind
class ContinuationStream:
"""Own only recovery-time mappings; ordinary events remain untouched."""
def __init__(self, delivered: DeliveredResponse) -> None:
self.delivered = delivered
self.active = False
self.handoff = False
self._offset = 0
self._namespace = uuid4()
self._boundary: list[dict[str, Any]] = []
self._expected = ""
self._overlap = ContinuationOverlap("")
self._initial_progress = 0
self._prefix_items: list[dict[str, Any]] = []
@property
def made_progress(self) -> bool:
return self.delivered.progress > self._initial_progress
def begin(self, *, handoff: bool) -> None:
self.active = True
self.handoff = handoff
self._offset = max(self.delivered.items, default=-1) + 1
self._boundary = self.delivered.closing_events()
self._expected = self.delivered.snapshot().text
self.restart_attempt()
finalized = deepcopy(self.delivered.items)
for event in self._boundary:
if event["type"] == "response.output_item.done":
finalized[event["output_index"]] = deepcopy(event["item"])
self._prefix_items = [item for _, item in sorted(finalized.items())]
def restart_attempt(self) -> None:
"""Replace an invisible attempt without losing its finalized prefix."""
self._namespace = uuid4()
self._overlap = ContinuationOverlap(self._expected)
self._initial_progress = self.delivered.progress
def boundary(self) -> list[dict[str, Any]]:
events, self._boundary = self._boundary, []
return [self._sequence(event) for event in events]
def _sequence(self, event: dict[str, Any]) -> dict[str, Any]:
if self.delivered.wire_api == "responses":
self.delivered.sequence += 1
event["sequence_number"] = self.delivered.sequence
return event
def _item_id(self, value: str) -> str:
return f"{value.split('_')[0]}_{uuid5(self._namespace, value).hex}"
def prepare(self, payload: dict[str, Any]) -> list[dict[str, Any]]:
if not self.active:
return [payload]
if (
self.delivered.wire_api == "responses"
and payload.get("type")
not in {"response.failed", "response.error", "error"}
and responses_text_constraint(payload)
):
raise ExecutionFailure(
kind=FailureKind.UPSTREAM,
status_code=502,
message="The continuation returned text with a native output contract that cannot be reconstructed.",
retryable=False,
)
if payload.get("type") in {
"message_start",
"response.created",
"response.in_progress",
}:
return []
events = [deepcopy(payload)] if self.handoff else self._overlap.feed(payload)
return [self._map(event) for event in events]
def _map(self, data: dict[str, Any]) -> dict[str, Any]:
if not self.handoff:
if isinstance(data.get("index"), int):
data["index"] += self._offset
if isinstance(data.get("output_index"), int):
data["output_index"] += self._offset
if isinstance(data.get("item_id"), str):
data["item_id"] = self._item_id(data["item_id"])
item = data.get("item")
if isinstance(item, dict) and isinstance(item.get("id"), str):
item["id"] = self._item_id(item["id"])
if "response_id" in data:
data["response_id"] = self.delivered.header.get("id", data["response_id"])
response = data.get("response")
if isinstance(response, dict):
response["id"] = self.delivered.header.get("id", response.get("id"))
if "created_at" in self.delivered.header:
response["created_at"] = self.delivered.header["created_at"]
if not self.handoff:
for item in response.get("output", []):
if isinstance(item.get("id"), str):
item["id"] = self._item_id(item["id"])
return self._sequence(data)
def finalize(self, data: dict[str, Any]) -> dict[str, Any]:
"""Finish the response only after preceding frames have been published."""
if not self.active:
return data
kind = data.get("type")
response = data.get("response")
output = response.get("output", []) if isinstance(response, dict) else []
terminal = kind in {"response.completed", "response.incomplete", "message_stop"}
terminal |= (
kind == "message_delta"
and data.get("delta", {}).get("stop_reason") is not None
)
if (
terminal
and not self.handoff
and not (self.made_progress or self._has_output(output))
):
raise ExecutionFailure(
kind=FailureKind.UPSTREAM,
status_code=502,
message="The provider continuation completed without adding output.",
retryable=False,
)
if isinstance(response, dict):
response["output"] = [
*deepcopy(self._prefix_items),
*([] if self.handoff else output),
]
response["usage"] = self.delivered.usage(response["output"])
elif kind == "message_delta" and "usage" in data:
data["usage"] = {"output_tokens": self.delivered.output_tokens()}
return data
@staticmethod
def _has_output(items: list[dict[str, Any]]) -> bool:
return any(
client_call(item)
or any(
part.get("text") or part.get("refusal")
for part in [*(item.get("content") or []), *(item.get("summary") or [])]
)
for item in items
)

View file

@ -0,0 +1,555 @@
"""Bounded, request-local evidence of output released to a stream consumer."""
from copy import deepcopy
from dataclasses import dataclass
from typing import Any, Literal
import simplejson
from .history_replay import (
ReplayRecord,
encode_replay,
readable_reasoning,
unencrypted_responses_replay,
)
from .openai_responses import is_client_search
from .token_estimation import estimate_text_tokens
RECOVERY_RETENTION_BYTES = 1_048_576
CLIENT_CALL_TYPES = frozenset(
{
"function_call",
"custom_tool_call",
"computer_call",
"local_shell_call",
"apply_patch_call",
}
)
def client_call(item: dict[str, Any]) -> bool:
kind = item.get("type")
if kind == "tool_search_call":
return is_client_search(item)
return kind in CLIENT_CALL_TYPES or (
kind == "shell_call"
and isinstance(item.get("environment"), dict)
and item["environment"].get("type") == "local"
)
def is_structured_text_format(value: object) -> bool:
return isinstance(value, dict) and value.get("type") in (
"json_schema",
"json_object",
)
def responses_text_constraint(data: dict[str, Any]) -> str | None:
"""Identify public text contracts that cannot survive synthetic completion."""
response = data.get("response")
response = response if isinstance(response, dict) else {}
text = response.get("text")
if isinstance(text, dict) and is_structured_text_format(text.get("format")):
return "structured_output"
part = data.get("part")
if data.get("logprobs") or (isinstance(part, dict) and part.get("logprobs")):
return "text_logprobs"
items = [data.get("item"), *(response.get("output") or [])]
if any(
part.get("logprobs")
for item in items
if isinstance(item, dict)
for part in item.get("content") or []
):
return "text_logprobs"
return None
@dataclass(frozen=True, slots=True)
class DeliveredPrefix:
text: str
thinking: str
has_calls: bool
eligible: bool
can_handoff: bool
unsafe_reason: str | None
class DeliveredResponse:
"""Track this response only; the harness continues to own conversation history."""
def __init__(self, wire_api: Literal["messages", "responses"]) -> None:
self.wire_api = wire_api
self.items: dict[int, dict[str, Any]] = {}
self.closed: set[int] = set()
self._text_closed: set[tuple[int, str, int]] = set()
self._part_closed: set[tuple[int, str, int]] = set()
self.header: dict[str, Any] = {}
self.sequence = -1
self.unsafe_reason: str | None = None
self._text_constraint: str | None = None
self._bytes = 0
self._limited = False
self._limited_output_tokens = 0
self.progress = 0
def _retain(self, value: object) -> bool:
if self._limited:
return False
self._bytes += len(
simplejson.dumps(value, use_decimal=True, ensure_ascii=False).encode(
"utf-8"
)
)
if self._bytes > RECOVERY_RETENTION_BYTES:
self._limited_output_tokens = self.output_tokens()
self._limited = True
self.unsafe_reason = "retention_limit"
self.items.clear()
self.closed.clear()
self._text_closed.clear()
self._part_closed.clear()
return False
return True
@property
def retention_exhausted(self) -> bool:
return self._limited
def observe(self, data: dict[str, Any], *, count_progress: bool = True) -> None:
kind = data.get("type", "")
if not isinstance(kind, str):
self.unsafe_reason = "unknown_event"
return
sequence = data.get("sequence_number")
if isinstance(sequence, int):
self.sequence = max(self.sequence, sequence)
if kind in {"message_start", "response.created"} and not self.header:
self.header = deepcopy(data.get("message", data.get("response", {})))
delta = data.get("delta", "")
text = (
delta
if isinstance(delta, str)
else "".join(
delta.get(field, "") for field in ("text", "thinking", "partial_json")
)
if isinstance(delta, dict)
else ""
)
block = data.get("content_block", {})
if kind == "content_block_start":
text += block.get("text", "") + block.get("thinking", "")
if (
block.get("type") == "redacted_thinking"
and (record := unencrypted_responses_replay(block.get("data")))
is not None
):
text += "".join(text for text, _ in readable_reasoning(record.native))
if count_progress and (
text
or self._new_snapshot_text(data)
or data.get("part", {}).get("refusal")
or (kind == "response.refusal.done" and data.get("refusal"))
or (kind == "content_block_start" and block.get("type") == "tool_use")
or (
kind == "response.output_item.done"
and client_call(data.get("item", {}))
)
):
self.progress += 1
if not self._limited:
if self.wire_api == "messages":
self._messages(kind, data)
else:
self._responses(kind, data)
if self._limited:
self._limited_output_tokens += estimate_text_tokens(text)
def _new_snapshot_text(self, data: dict[str, Any]) -> bool:
"""Cumulative text is progress only when it adds published characters."""
kind = data.get("type")
recorded = self.items.get(data.get("output_index"), {})
previous = recorded.get("content") or []
if kind in {"response.output_item.added", "response.output_item.done"}:
item = data.get("item", {})
if item.get("type") != "message":
return False
parts = enumerate(item.get("content") or [])
elif kind in {"response.content_part.added", "response.content_part.done"}:
parts = [(data.get("content_index", 0), data.get("part", {}))]
elif kind == "response.output_text.done":
parts = [
(
data.get("content_index", 0),
{"type": "output_text", "text": data.get("text", "")},
)
]
else:
return False
return any(
part.get("type") == "output_text"
and len(part.get("text", ""))
> len(previous[index].get("text", "") if index < len(previous) else "")
for index, part in parts
)
def _messages(self, kind: str, data: dict[str, Any]) -> None:
index = data.get("index")
if not isinstance(index, int):
if kind not in {
"message_start",
"message_delta",
"message_stop",
"ping",
"error",
}:
self.unsafe_reason = "unknown_event"
return
if kind == "content_block_start":
block = data.get("content_block", {})
if not self._retain(block):
return
self.items[index] = deepcopy(block)
if block.get("type") not in {
"text",
"thinking",
"redacted_thinking",
"tool_use",
}:
self.unsafe_reason = "native_content"
if block.get("signature") or block.get("type") == "redacted_thinking":
value = (
block.get("data")
if block.get("type") == "redacted_thinking"
else block.get("signature")
)
if unencrypted_responses_replay(value) is None:
self.unsafe_reason = "opaque_reasoning"
elif kind == "content_block_delta":
delta = data.get("delta", {})
if not self._retain(delta):
return
block = self.items.get(index)
if block is None:
self.unsafe_reason = "missing_block"
return
field = {
"text_delta": "text",
"thinking_delta": "thinking",
"input_json_delta": "partial_json",
}.get(delta.get("type"))
if field:
block[field] = block.get(field, "") + delta.get(field, "")
elif delta.get("type") == "signature_delta":
block["signature"] = block.get("signature", "") + delta.get(
"signature", ""
)
if unencrypted_responses_replay(block["signature"]) is None:
self.unsafe_reason = "opaque_delta"
else:
self.unsafe_reason = "opaque_delta"
elif kind == "content_block_stop":
self.closed.add(index)
def _responses(self, kind: str, data: dict[str, Any]) -> None:
self._text_constraint = self._text_constraint or responses_text_constraint(data)
index = data.get("output_index")
if not isinstance(index, int):
if kind not in {
"response.created",
"response.in_progress",
"response.completed",
"response.incomplete",
"response.failed",
"response.error",
"error",
"ping",
}:
self.unsafe_reason = "unknown_event"
return
if kind in {"response.output_item.added", "response.output_item.done"}:
item = data.get("item", {})
if not self._retain(item):
return
self.items[index] = deepcopy(item)
if item.get("type") not in {"message", "reasoning"} and not client_call(
item
):
self.unsafe_reason = "native_content"
if (
item.get("encrypted_content")
and unencrypted_responses_replay(item["encrypted_content"]) is None
):
self.unsafe_reason = "opaque_reasoning"
if any(part.get("type") == "refusal" for part in item.get("content") or []):
self.unsafe_reason = "refusal"
if kind.endswith(".done"):
self.closed.add(index)
elif kind in {
"response.content_part.added",
"response.reasoning_summary_part.added",
"response.content_part.done",
"response.reasoning_summary_part.done",
}:
part = data.get("part", {})
if not self._retain(part) or (
kind.endswith(".done")
and not self._retain(
(
kind,
index,
data.get("content_index", data.get("summary_index", 0)),
)
)
):
return
item = self.items.get(index)
if item is None:
self.unsafe_reason = "missing_item"
return
field = "summary" if "reasoning" in kind else "content"
position = data.get(
"summary_index" if field == "summary" else "content_index", 0
)
parts = item[field] = item.get(field) or []
while len(parts) <= position:
parts.append({})
parts[position] = deepcopy(part)
if kind.endswith(".done"):
self._part_closed.add((index, field, position))
if part.get("type") == "refusal":
self.unsafe_reason = "refusal"
elif part.get("type") not in {
"output_text",
"summary_text",
"reasoning_text",
}:
self.unsafe_reason = "native_content"
elif kind in {
"response.output_text.delta",
"response.reasoning_summary_text.delta",
"response.reasoning_text.delta",
"response.refusal.delta",
"response.output_text.done",
"response.reasoning_summary_text.done",
"response.reasoning_text.done",
"response.refusal.done",
}:
done = kind.endswith(".done")
text_field = "refusal" if "refusal" in kind else "text"
delta = data.get(text_field if done else "delta", "")
if not self._retain(delta) or (
done
and not self._retain(
(
kind,
index,
data.get("content_index", data.get("summary_index", 0)),
)
)
):
return
item = self.items.get(index)
if item is None:
self.unsafe_reason = "missing_item"
return
field = "summary" if "reasoning_summary" in kind else "content"
position = data.get(
"summary_index" if field == "summary" else "content_index", 0
)
parts = item[field] = item.get(field) or []
while len(parts) <= position:
parts.append(
{
"type": "summary_text"
if field == "summary"
else "reasoning_text"
if "reasoning" in kind
else "refusal"
if text_field == "refusal"
else "output_text",
text_field: "",
}
)
parts[position][text_field] = (
delta if done else parts[position].get(text_field, "") + delta
)
if done:
self._text_closed.add((index, field, position))
if text_field == "refusal":
self.unsafe_reason = "refusal"
elif kind.endswith(".delta") and kind not in {
"response.function_call_arguments.delta",
"response.custom_tool_call_input.delta",
}:
self.unsafe_reason = "native_delta"
elif kind not in {
"response.function_call_arguments.delta",
"response.function_call_arguments.done",
"response.custom_tool_call_input.delta",
"response.custom_tool_call_input.done",
}:
self.unsafe_reason = "unknown_event"
def snapshot(self) -> DeliveredPrefix:
text: list[str] = []
thinking: list[str] = []
has_calls = False
for index, item in sorted(self.items.items()):
kind = item.get("type")
if self.wire_api == "messages":
if kind == "text":
text.append(item.get("text", ""))
elif kind == "thinking":
thinking.append(item.get("thinking", ""))
elif kind == "redacted_thinking":
if (
record := unencrypted_responses_replay(item.get("data"))
) is not None:
thinking.extend(
text for text, _ in readable_reasoning(record.native)
)
elif kind == "tool_use" and index in self.closed:
has_calls = True
elif kind == "message":
text.extend(part.get("text", "") for part in item.get("content") or [])
elif kind == "reasoning":
thinking.extend(
part.get("text", "")
for part in [
*(item.get("summary") or []),
*(item.get("content") or []),
]
)
elif client_call(item) and index in self.closed:
has_calls = True
unsafe_reason = self._text_constraint or self.unsafe_reason
return DeliveredPrefix(
"".join(text),
"".join(thinking),
has_calls,
unsafe_reason is None,
self.can_handoff,
unsafe_reason,
)
def output_tokens(self, output: list[dict[str, Any]] | None = None) -> int:
if output is None and self._limited:
return self._limited_output_tokens
return estimate_text_tokens(
simplejson.dumps(
list(self.items.values()) if output is None else output,
use_decimal=True,
ensure_ascii=False,
)
)
@property
def can_handoff(self) -> bool:
if self._text_constraint or self.unsafe_reason not in {
None,
"opaque_reasoning",
"opaque_delta",
}:
return False
return all(
index in self.closed
for index, item in self.items.items()
if item.get("type") in {"thinking", "redacted_thinking", "reasoning"}
)
def closing_events(self) -> list[dict[str, Any]]:
events: list[dict[str, Any]] = []
for index, item in sorted(self.items.items()):
if index in self.closed:
continue
if self.wire_api == "messages":
events.append({"type": "content_block_stop", "index": index})
else:
for field in ("content", "summary"):
for position, part in enumerate(item.get(field) or []):
key = (index, field, position)
if key in self._part_closed:
continue
summary = field == "summary"
identity = {
"item_id": item["id"],
"output_index": index,
"summary_index" if summary else "content_index": position,
}
if key not in self._text_closed:
text_kind = (
"response.reasoning_summary_text.done"
if summary
else "response.reasoning_text.done"
if part.get("type") == "reasoning_text"
else "response.output_text.done"
)
events.append(
{
"type": text_kind,
**identity,
"text": part.get("text", ""),
}
)
if part.get("type") != "reasoning_text":
events.append(
{
"type": "response.reasoning_summary_part.done"
if summary
else "response.content_part.done",
**identity,
"part": deepcopy(part),
}
)
closed_item = {**deepcopy(item), "status": "completed"}
if (
record := unencrypted_responses_replay(
item.get("encrypted_content")
)
) is not None:
native = {**record.native, "status": "completed"}
for field in ("content", "summary"):
if field in closed_item:
native[field] = deepcopy(closed_item[field])
closed_item["encrypted_content"] = encode_replay(
ReplayRecord(record.origin, native)
)
events.append(
{
"type": "response.output_item.done",
"output_index": index,
"item": closed_item,
}
)
return events
def handoff_events(self) -> list[dict[str, Any]]:
"""Signal handoff; the public writer fills output and usage after closure."""
if self.wire_api == "messages":
return [
{
"type": "message_delta",
"delta": {"stop_reason": "tool_use", "stop_sequence": None},
"usage": {},
},
{"type": "message_stop"},
]
response = {
**deepcopy(self.header),
"status": "completed",
}
return [{"type": "response.completed", "response": response}]
def usage(self, output: list[dict[str, Any]] | None = None) -> dict[str, Any]:
initial = deepcopy(self.header.get("usage") or {})
input_tokens = initial.get("input_tokens", 0)
output_tokens = self.output_tokens(output)
return {
**initial,
"input_tokens": input_tokens,
"output_tokens": output_tokens,
"total_tokens": input_tokens + output_tokens,
}

View file

@ -314,6 +314,19 @@ def has_readable_replay(value: object) -> bool:
)
def unencrypted_responses_replay(value: object) -> ReplayRecord | None:
"""Identify local Responses carriers without native encrypted reasoning."""
if not isinstance(value, str) or not is_replay(value):
return None
try:
record = decode_replay(value)
except HistoryReplayError:
return None
if record.origin.protocol != "responses" or record.native.get("encrypted_content"):
return None
return record
def _replay_readable(
item: Mapping[str, JsonValue], record: ReplayRecord | None
) -> list[tuple[str, bool]]:

View file

@ -50,6 +50,7 @@ class ResponsesProviderStream:
input_tokens: int,
log_raw_events: bool = False,
tool_names: OpenAIToolNameCodec | None = None,
pad_empty: bool = True,
) -> None:
self.ledger = AnthropicStreamLedger(
message_id,
@ -58,6 +59,7 @@ class ResponsesProviderStream:
log_raw_events=log_raw_events,
)
self.completed = False
self._pad_empty = pad_empty
self.generated_output = False
self._tool_names = tool_names or OpenAIToolNameCodec.from_names(())
self._tools: dict[str, _ToolState] = {}
@ -219,7 +221,7 @@ class ResponsesProviderStream:
response = data.get("response")
response = response if isinstance(response, dict) else {}
events = list(self.ledger.close_all_blocks())
if not self.ledger.has_content_block():
if self._pad_empty and not self.ledger.has_content_block():
events.extend(self.ledger.ensure_text_block())
events.append(self.ledger.emit_text_delta(" "))
events.append(self.ledger.stop_text_block())

View file

@ -1,16 +1,22 @@
"""Request-local evidence of public stream delivery, independent of wire format."""
from collections.abc import Iterator
from collections.abc import Callable, Iterator
from contextlib import contextmanager
from contextvars import ContextVar
from typing import Literal
from .delivered_response import DeliveredPrefix
class StreamDeliveryState:
"""Retain public commitment while identifying invisible generation attempts."""
"""Exchange attempt transitions and immutable public-delivery evidence."""
def __init__(self) -> None:
self._content_released = False
self._attempt_content_released = False
self._attempt_revision = 0
self._prefix_reader: Callable[[], DeliveredPrefix] | None = None
self.transition: Literal["continue", "handoff"] | None = None
@property
def content_released(self) -> bool:
@ -20,12 +26,28 @@ class StreamDeliveryState:
def attempt_revision(self) -> int:
return self._attempt_revision
def release_content(self) -> None:
@property
def attempt_content_released(self) -> bool:
return self._attempt_content_released
@property
def prefix(self) -> DeliveredPrefix | None:
return self._prefix_reader() if self._prefix_reader is not None else None
def bind_prefix(self, reader: Callable[[], DeliveredPrefix] | None) -> None:
self._prefix_reader = reader
def release_content(self, *, synthetic: bool = False) -> None:
self._content_released = True
if not synthetic:
self._attempt_content_released = True
def begin_attempt(self) -> None:
if not self._content_released:
self._attempt_revision += 1
self._attempt_revision += 1
self._attempt_content_released = False
def begin_continuation(self, *, handoff: bool = False) -> None:
self.transition = "handoff" if handoff else "continue"
_delivery: ContextVar[StreamDeliveryState | None] = ContextVar(

View file

@ -143,7 +143,10 @@ class ProviderExecution:
) -> ProviderAttempt:
"""Open the sole active physical call for this execution."""
attempt = await self._controller._open_attempt(self, operation_kind)
if operation_kind is ProviderOperationKind.GENERATION and self._delivery:
if self._delivery and operation_kind in {
ProviderOperationKind.GENERATION,
ProviderOperationKind.CONTINUATION,
}:
self._delivery.begin_attempt()
return attempt

View file

@ -23,6 +23,11 @@ from free_claude_code.providers.admission import (
ProviderAdmissionController,
ProviderOperationKind,
)
from free_claude_code.providers.continuation import (
ContinuationRequest,
SourceRecoveryState,
public_recovery,
)
from free_claude_code.providers.failure_policy import (
RetryableProviderProtocolError,
classify_provider_failure,
@ -81,11 +86,14 @@ async def stream_native_messages(
)
return
committed = False
continuation_request = ContinuationRequest("messages")
operation_kind = ProviderOperationKind.GENERATION
while execution.can_attempt:
normal_stop_seen = False
source = SourceRecoveryState(execution)
scope = None
try:
attempt = await execution.open_attempt(ProviderOperationKind.GENERATION)
attempt = await execution.open_attempt(operation_kind)
scope = ProviderAttemptScope(
attempt, provider_name=provider_name, request_id=request_id
)
@ -113,6 +121,11 @@ async def stream_native_messages(
async for kind, payload in messages_events(response):
normal_stop_seen |= is_messages_stop(kind, payload)
check_messages_failure(kind, payload, native=True)
source.observe(
"messages",
payload,
continuing=operation_kind is ProviderOperationKind.CONTINUATION,
)
output = relay.feed(kind, payload)
if output is None:
continue
@ -155,6 +168,23 @@ async def stream_native_messages(
normal_stop_seen=normal_stop_seen,
):
continue
if scope is not None:
await scope.aclose(active_error=error)
recovered = public_recovery(
execution,
body=body,
request=continuation_request,
retryable=is_retryable_stream_error(error),
normal_stop_seen=normal_stop_seen,
source=source,
)
if recovered is not None:
if recovered.body is None:
execution.succeed()
return
body = recovered.body
operation_kind = ProviderOperationKind.CONTINUATION
continue
if error is not raw_error:
raise error from raw_error
raise

View file

@ -38,6 +38,11 @@ from free_claude_code.providers.admission import (
ProviderExecution,
ProviderOperationKind,
)
from free_claude_code.providers.continuation import (
ContinuationRequest,
SourceRecoveryState,
public_recovery,
)
from free_claude_code.providers.endpoint import RequestEndpoint
from free_claude_code.providers.endpoint_types import EndpointContext
from free_claude_code.providers.failure_policy import (
@ -275,13 +280,16 @@ class AnthropicMessagesTransport:
execution, endpoint=request_endpoint, stream=recovery
)
corrections = RequestCorrections("messages", reasoning_correction)
continuation_request = ContinuationRequest("messages")
operation_kind = ProviderOperationKind.GENERATION
while execution.can_attempt:
normal_stop_seen = False
source = SourceRecoveryState(execution)
scope: ProviderAttemptScope | None = None
stream_opened = False
sent_body = body
try:
attempt = await execution.open_attempt(ProviderOperationKind.GENERATION)
attempt = await execution.open_attempt(operation_kind)
scope = ProviderAttemptScope(
attempt,
provider_name=self._provider_name,
@ -336,6 +344,11 @@ class AnthropicMessagesTransport:
async for event_type, payload in messages_events(response):
normal_stop_seen |= is_messages_stop(event_type, payload)
check_messages_failure(event_type, payload)
source.observe(
"messages",
payload,
continuing=operation_kind is ProviderOperationKind.CONTINUATION,
)
output = presenter.feed(event_type, payload)
if event_type != "ping" and not attempt.accepted:
await attempt.accept()
@ -373,7 +386,7 @@ class AnthropicMessagesTransport:
status,
scope.attempt,
body,
operation_kind=ProviderOperationKind.GENERATION,
operation_kind=operation_kind,
normal_stop_seen=normal_stop_seen,
propose_correction=partial(
corrections.next_body,
@ -412,6 +425,24 @@ class AnthropicMessagesTransport:
if decision.action is RecoveryFailureAction.EARLY_RETRY:
recovery.discard()
continue
if scope is not None:
await scope.aclose(active_error=error)
recovered = public_recovery(
execution,
body=body,
request=continuation_request,
retryable=decision.retryable,
normal_stop_seen=normal_stop_seen,
source=source,
)
if recovered is not None:
recovery.discard()
if recovered.body is None:
return
body = recovered.body
operation_kind = ProviderOperationKind.CONTINUATION
corrections = RequestCorrections("messages", reasoning_correction)
continue
failure = classify_provider_failure(
error,
provider_name=self._provider_name,

View file

@ -0,0 +1,283 @@
"""Same-provider continuation using evidence from the public delivery boundary."""
from copy import deepcopy
from dataclasses import dataclass
from typing import Any, Literal
from free_claude_code.core.delivered_response import (
client_call,
is_structured_text_format,
)
from free_claude_code.core.failures import ExecutionFailure, FailureKind
from free_claude_code.core.history_replay import reasoning_context
from free_claude_code.core.trace import trace_event
from .admission import ProviderExecution
type UpstreamProtocol = Literal["chat", "messages", "responses"]
class SourceRecoveryState:
"""Native source evidence cannot be inferred from buffered public events."""
def __init__(self, execution: ProviderExecution) -> None:
self._execution = execution
self.unsafe_reason: str | None = None
self._pending_reasoning: set[int | str] = set()
self._signed: set[int | str] = set()
self._handoff_blocked = False
@property
def can_handoff(self) -> bool:
return not self._handoff_blocked and not self._pending_reasoning
def observe(
self,
protocol: UpstreamProtocol,
payload: dict[str, Any],
*,
continuing: bool = False,
) -> None:
kind = str(payload.get("type", ""))
usage = payload.get("usage")
if protocol == "messages" and kind == "message_start":
usage = payload.get("message", {}).get("usage")
elif protocol == "responses":
usage = payload.get("response", {}).get("usage")
if isinstance(usage, dict) and usage:
trace_event(
stage="provider",
event="provider.attempt.usage",
source="provider",
request_id=self._execution.request_id,
execution_id=self._execution.execution_id,
attempt=self._execution.attempts_started,
usage=usage,
)
if protocol == "chat":
for choice in payload.get("choices", []):
delta = choice.get("delta") or {}
if delta.get("reasoning_details") or delta.get("audio"):
self.unsafe_reason = "native_content"
self._handoff_blocked = True
elif protocol == "messages":
block = payload.get("content_block")
index = payload.get("index", -1)
if isinstance(block, dict) and block.get("type") in {
"thinking",
"redacted_thinking",
}:
self.unsafe_reason = "signed_thinking"
self._pending_reasoning.add(index)
if block.get("signature") or block.get("data"):
self._signed.add(index)
if isinstance(block, dict) and block.get("type") not in {
"text",
"tool_use",
"thinking",
"redacted_thinking",
}:
self.unsafe_reason = "native_content"
self._handoff_blocked = True
delta = payload.get("delta") or {}
if (isinstance(block, dict) and block.get("citations")) or delta.get(
"type"
) == "citations_delta":
self.unsafe_reason = "native_content"
self._handoff_blocked = True
if delta.get("type") in {"thinking_delta", "signature_delta"}:
self.unsafe_reason = "signed_thinking"
if delta.get("signature"):
self._signed.add(index)
if kind == "content_block_stop" and index in self._signed:
self._pending_reasoning.discard(index)
else:
item = payload.get("item")
if isinstance(item, dict):
self._response_item(item, completed=kind == "response.output_item.done")
response = payload.get("response")
if isinstance(response, dict):
for item in response.get("output", []):
if isinstance(item, dict):
self._response_item(
item, completed=item.get("status") == "completed"
)
part = payload.get("part")
if (
kind in {"response.content_part.added", "response.content_part.done"}
and isinstance(part, dict)
and part.get("annotations")
):
self.unsafe_reason = "native_content"
self._handoff_blocked = True
if kind.startswith(
(
"response.web_search",
"response.code_interpreter",
"response.mcp_",
"response.image_generation",
"response.audio",
)
):
self.unsafe_reason = "hosted_or_native_content"
self._handoff_blocked = True
if kind == "response.output_text.annotation.added":
self.unsafe_reason = "native_content"
self._handoff_blocked = True
if continuing and self.unsafe_reason:
raise ExecutionFailure(
kind=FailureKind.UPSTREAM,
status_code=502,
message="The continuation returned native state that cannot be combined with the delivered response.",
retryable=False,
)
def _response_item(self, item: dict[str, Any], *, completed: bool) -> None:
if item.get("type") not in {"message", "reasoning"} and not client_call(item):
self.unsafe_reason = "hosted_or_native_content"
self._handoff_blocked = True
if item.get("encrypted_content"):
self.unsafe_reason = "opaque_reasoning"
if not completed:
self._pending_reasoning.add(item.get("id", ""))
if completed:
self._pending_reasoning.discard(item.get("id", ""))
if any(
part.get("annotations")
or part.get("type")
not in {"output_text", "refusal", "reasoning_text", "summary_text"}
for part in [*(item.get("content") or []), *(item.get("summary") or [])]
):
self.unsafe_reason = "native_content"
self._handoff_blocked = True
@dataclass(frozen=True, slots=True)
class PublicRecovery:
body: dict[str, Any] | None = None
class ContinuationRequest:
"""Replace our private history suffix while retaining accepted corrections."""
def __init__(self, protocol: UpstreamProtocol) -> None:
self.protocol = protocol
self._injected_rows = 0
def build(self, body: dict[str, Any], text: str, thinking: str) -> dict[str, Any]:
result = deepcopy(body)
responses = self.protocol == "responses"
field = "input" if responses else "messages"
history = result.get(field, [])
if responses and isinstance(history, str):
history = [{"role": "user", "content": history}]
if self._injected_rows:
# Request corrections preserve these trailing plain-text turns.
history = history[: -self._injected_rows]
instruction = "The previous provider stream was interrupted. Continue the assistant response exactly where it stopped. Do not repeat text already written."
if result.get("tools"):
instruction += " No tool calls from this interrupted assistant turn were delivered or executed. Any unfinished calls were discarded. Emit the required tool calls using the provided tools."
if thinking:
instruction = f"{reasoning_context(thinking)}\n\n{instruction}"
tail = []
if text:
tail.append(
{
"role": "assistant",
"content": [{"type": "output_text", "text": text}]
if responses
else text,
}
)
tail.append(
{
"role": "user",
"content": [{"type": "input_text", "text": instruction}]
if responses
else instruction,
}
)
result[field] = [*history, *tail]
self._injected_rows = len(tail)
return result
def _structured_output(body: dict[str, Any], protocol: UpstreamProtocol) -> bool:
if protocol == "chat":
extra = body.get("extra_body") or {}
value = extra.get("response_format", body.get("response_format"))
else:
options = body.get("text" if protocol == "responses" else "output_config") or {}
value = options.get("format")
return is_structured_text_format(value)
def public_recovery(
execution: ProviderExecution,
*,
body: dict[str, Any],
request: ContinuationRequest,
retryable: bool,
normal_stop_seen: bool,
source: SourceRecoveryState,
) -> PublicRecovery | None:
delivery = execution.delivery
if delivery is None or not delivery.content_released:
return None
if not retryable or normal_stop_seen:
return None
prefix = delivery.prefix
if prefix is None:
return None
structured = _structured_output(body, request.protocol)
handoff = (
not structured
and prefix.has_calls
and source.can_handoff
and prefix.can_handoff
)
if not handoff and (structured or source.unsafe_reason or not prefix.eligible):
trace_event(
stage="provider",
event="provider.recovery.ineligible",
source="provider",
request_id=execution.request_id,
reason="structured_output"
if structured
else source.unsafe_reason or prefix.unsafe_reason,
)
return None
if handoff:
delivery.begin_continuation(handoff=True)
trace_event(
stage="provider",
event="provider.recovery.tool_handoff",
source="provider",
request_id=execution.request_id,
usage_estimated=True,
)
return PublicRecovery()
if not execution.can_attempt:
trace_event(
stage="provider",
event="provider.recovery.exhausted",
source="provider",
request_id=execution.request_id,
attempts_started=execution.attempts_started,
max_attempts=execution.max_attempts,
)
return None
if not (prefix.text or prefix.thinking):
return None
replacement = request.build(body, prefix.text, prefix.thinking)
delivery.begin_continuation()
trace_event(
stage="provider",
event="provider.recovery.continuation",
source="provider",
request_id=execution.request_id,
attempts_started=execution.attempts_started,
max_attempts=execution.max_attempts,
usage_estimated=True,
)
return PublicRecovery(body=replacement)

View file

@ -60,6 +60,11 @@ from free_claude_code.providers.admission import (
ProviderAttempt,
ProviderOperationKind,
)
from free_claude_code.providers.continuation import (
ContinuationRequest,
SourceRecoveryState,
public_recovery,
)
from free_claude_code.providers.endpoint import RequestEndpoint
from free_claude_code.providers.endpoint_types import EndpointContext
from free_claude_code.providers.failure_policy import (
@ -393,7 +398,7 @@ class _OpenAIChatStreamAssembler:
yield from self._output.flush_reasoning_replay()
self._upstream_finished = True
def prepare_completion(self) -> Iterator[str]:
def prepare_completion(self, *, pad_empty: bool) -> Iterator[str]:
if not self._upstream_finished or self._completion is not None:
raise RuntimeError("stream completion cannot be prepared")
@ -407,10 +412,13 @@ class _OpenAIChatStreamAssembler:
has_emitted_tool = self._output.has_emitted_tool_block()
has_content_blocks = self._output.has_content_block()
if not has_content_blocks or (
not has_emitted_tool
and not self._output.accumulated_text.strip()
and self._output.accumulated_reasoning.strip()
if pad_empty and (
not has_content_blocks
or (
not has_emitted_tool
and not self._output.accumulated_text.strip()
and self._output.accumulated_reasoning.strip()
)
):
yield from self._output.ensure_text_block()
yield self._output.emit_text_delta(" ")
@ -941,6 +949,8 @@ class _OpenAIChatStreamRunner:
yield from recovery.push(event)
body = self._body
continuation_request = ContinuationRequest("chat")
operation_kind = ProviderOperationKind.GENERATION
request_stream_usage(body)
output_reasoning = self._reasoning.output_enabled
corrections = RequestCorrections("chat", self._reasoning_correction)
@ -960,17 +970,58 @@ class _OpenAIChatStreamRunner:
while True:
assembler = self._new_stream_assembler(output_reasoning=output_reasoning)
source = SourceRecoveryState(execution)
def observe_chunk(
chunk: Any,
assembler: _OpenAIChatStreamAssembler = assembler,
source: SourceRecoveryState = source,
operation_kind: ProviderOperationKind = operation_kind,
) -> None:
assembler.observe_upstream_chunk(chunk)
source.observe(
"chat",
{
"usage": {
key: value
for key in (
"prompt_tokens",
"completion_tokens",
"total_tokens",
)
if (value := usage_int(getattr(chunk, "usage", None), key))
is not None
},
"choices": [
{
"delta": {
"reasoning_details": getattr(
getattr(choice, "delta", None),
"reasoning_details",
None,
),
"audio": getattr(
getattr(choice, "delta", None), "audio", None
),
}
}
for choice in (getattr(chunk, "choices", None) or ())
],
},
continuing=operation_kind is ProviderOperationKind.CONTINUATION,
)
scope: ProviderAttemptScope | None = None
try:
stream, body, attempt, sent_body = await self._transport._create_stream(
body,
request_recovery,
ProviderOperationKind.GENERATION,
operation_kind,
corrections=corrections,
endpoint=self._endpoint,
request_client=self._request_client,
extra_headers=self._extra_headers,
on_event=assembler.observe_upstream_chunk,
on_event=observe_chunk,
)
scope = ProviderAttemptScope(
attempt,
@ -1012,7 +1063,7 @@ class _OpenAIChatStreamRunner:
provider_authentication_status(error),
scope.attempt,
body,
operation_kind=ProviderOperationKind.GENERATION,
operation_kind=operation_kind,
normal_stop_seen=assembler.normal_stop_seen,
propose_correction=partial(
corrections.next_body,
@ -1027,6 +1078,30 @@ class _OpenAIChatStreamRunner:
body = corrected_body
recovery.discard()
continue
if (
execution.delivery is not None
and execution.delivery.content_released
):
if scope is not None:
await scope.aclose(active_error=error)
recovered = public_recovery(
execution,
body=body,
request=continuation_request,
retryable=is_retryable_stream_error(error),
normal_stop_seen=assembler.normal_stop_seen,
source=source,
)
if recovered is not None:
recovery.discard()
if recovered.body is None:
return
body = recovered.body
operation_kind = ProviderOperationKind.CONTINUATION
corrections = RequestCorrections(
"chat", self._reasoning_correction
)
continue
resolution = await self._resolve_attempt_failure(
error=error,
scope=scope,
@ -1052,7 +1127,9 @@ class _OpenAIChatStreamRunner:
if scope is not None:
await scope.aclose(active_error=sys.exception())
for event in assembler.prepare_completion():
for event in assembler.prepare_completion(
pad_empty=operation_kind is ProviderOperationKind.GENERATION
):
for out_event in hold_event(event):
yield out_event
completion = assembler.completion
@ -1164,7 +1241,10 @@ class _OpenAIChatStreamRunner:
)
return _OpenAIChatFailureResolution(outcome=_OpenAIChatFailureOutcome.RETRY)
if decision.action == RecoveryFailureAction.MIDSTREAM_RECOVERY:
if (
decision.action == RecoveryFailureAction.MIDSTREAM_RECOVERY
and execution.delivery is None
):
if scope is not None:
await scope.aclose(active_error=error)
try:

View file

@ -112,4 +112,4 @@ class ResponsesExecutionOutcome:
failure: Exception | None = None
type ResponsesPresenterFactory = Callable[[], ResponsesStreamPresenter]
type ResponsesPresenterFactory = Callable[[bool], ResponsesStreamPresenter]

View file

@ -42,6 +42,11 @@ from free_claude_code.providers.admission import (
ProviderExecution,
ProviderOperationKind,
)
from free_claude_code.providers.continuation import (
ContinuationRequest,
SourceRecoveryState,
public_recovery,
)
from free_claude_code.providers.endpoint import RequestEndpoint
from free_claude_code.providers.endpoint_types import EndpointContext
from free_claude_code.providers.failure_policy import (
@ -155,13 +160,14 @@ class OpenAIResponsesTransport:
extra_headers=dict(extra_headers or {}),
request_id=request_id,
response_model=response_model,
presenter_factory=lambda: MessagesResponsesPresenter(
presenter_factory=lambda pad_empty: MessagesResponsesPresenter(
ResponsesProviderStream(
message_id=message_id,
model=response_model,
input_tokens=input_tokens,
tool_names=tool_names,
log_raw_events=self._log_raw_sse_events,
pad_empty=pad_empty,
)
),
)
@ -185,7 +191,7 @@ class OpenAIResponsesTransport:
extra_headers=dict(extra_headers or {}),
request_id=request_id,
response_model=response_model,
presenter_factory=lambda: NativeResponsesPresenter(
presenter_factory=lambda _pad_empty: NativeResponsesPresenter(
public_model=response_model, tool_events=tools.event_adapter()
),
)
@ -318,6 +324,8 @@ class OpenAIResponsesTransport:
execution, endpoint=endpoint, stream=recovery
)
corrections = RequestCorrections("responses", reasoning_correction)
continuation_request = ContinuationRequest("responses")
operation_kind = ProviderOperationKind.GENERATION
trace_event(
stage="provider",
event="provider.request.sent",
@ -332,7 +340,10 @@ class OpenAIResponsesTransport:
while execution.can_attempt:
normal_stop_seen = False
presenter = presenter_factory()
source = SourceRecoveryState(execution)
presenter = presenter_factory(
operation_kind is ProviderOperationKind.GENERATION
)
start_events = tuple(presenter.start())
presenter_started = False
adapt_event = (
@ -357,7 +368,7 @@ class OpenAIResponsesTransport:
endpoint=endpoint.snapshot if endpoint is not None else None,
)
sent_body = prepare_history(body, origin)
attempt = await execution.open_attempt(ProviderOperationKind.GENERATION)
attempt = await execution.open_attempt(operation_kind)
scope = ProviderAttemptScope(
attempt,
provider_name=self._provider_name,
@ -388,11 +399,23 @@ class OpenAIResponsesTransport:
JsonObject,
upstream_event.to_dict(mode="json"),
)
if upstream_event.type in {
failed = upstream_event.type in {
"response.failed",
"error",
"response.error",
}:
}
context_exceeded = reports_context_window_incomplete(
upstream_event.type, payload
)
# Failed snapshots constrain recovery without replacing the
# provider's own error with an output-eligibility failure.
source.observe(
"responses",
payload,
continuing=operation_kind is ProviderOperationKind.CONTINUATION
and not (failed or context_exceeded),
)
if failed:
stream_failure = responses_stream_failure_from_event(
upstream_event.type,
payload,
@ -408,10 +431,7 @@ class OpenAIResponsesTransport:
# cannot retain a consistent public identity.
stream_failure.payload = None
raise stream_failure
if reports_context_window_incomplete(
upstream_event.type,
payload,
):
if context_exceeded:
raise context_window_exceeded_provider_failure()
if adapt_event is not None:
payload = adapt_event(upstream_event.type, payload)
@ -453,7 +473,7 @@ class OpenAIResponsesTransport:
provider_authentication_status(error),
scope.attempt,
body,
operation_kind=ProviderOperationKind.GENERATION,
operation_kind=operation_kind,
normal_stop_seen=normal_stop_seen,
propose_correction=partial(
corrections.next_body,
@ -504,6 +524,25 @@ class OpenAIResponsesTransport:
)
continue
if scope is not None:
await scope.aclose(active_error=error)
recovered = public_recovery(
execution,
body=body,
request=continuation_request,
retryable=decision.retryable,
normal_stop_seen=normal_stop_seen,
source=source,
)
if recovered is not None:
recovery.discard()
if recovered.body is None:
return
body = recovered.body
operation_kind = ProviderOperationKind.CONTINUATION
corrections = RequestCorrections("responses", reasoning_correction)
continue
failure = classify_provider_failure(
error,
provider_name=self._provider_name,

View file

@ -76,18 +76,23 @@ class RequestRecovery:
) -> JsonObject | None:
delivery = self.execution.delivery
if (
operation_kind is ProviderOperationKind.GENERATION
and normal_stop_seen
normal_stop_seen
and delivery is not None
and not delivery.content_released
and (
not delivery.content_released
or operation_kind is ProviderOperationKind.CONTINUATION
)
):
return None
if await self.retry_authentication(error, auth_status, attempt):
return body
if operation_kind is ProviderOperationKind.GENERATION and self._committed:
if (
delivery.attempt_content_released
if delivery is not None
else operation_kind is ProviderOperationKind.GENERATION and self._committed
):
return None
# A separately buffered continuation/repair body can still be corrected
# at creation, while authentication follows the original public stream.
# Correct only an attempt whose public content can still be replaced.
corrected = propose_correction()
if corrected is not None and await self._authorize(error, attempt):
return corrected

View file

@ -0,0 +1,451 @@
"""Continuation replay is independent of how source text is represented."""
import asyncio
from copy import deepcopy
from itertools import pairwise
import pytest
from free_claude_code.core.anthropic.stream_contracts import parse_sse_text
from free_claude_code.core.stream_delivery import current_stream_delivery
from free_claude_code.providers.stream_recovery import RecoveryHoldbackBuffer
from tests.api.test_delivered_stream_recovery import public_text, text_events
from tests.api.test_hidden_stream_retries import delivered
from tests.api.test_tool_call_buffer import _frames, _response
from tests.providers.test_history_transports import _harness
@pytest.fixture(autouse=True)
def immediate_events(monkeypatch):
monkeypatch.setattr(
"free_claude_code.providers.stream_recovery.RecoveryHoldbackBuffer",
lambda: RecoveryHoldbackBuffer(max_bytes=1),
)
def responses_parts(parts, *, separate_items=False, representation="deltas"):
"""Describe one answer with consistent source snapshots at every level."""
template = text_events("responses", "")
items = []
events = [template[0]]
groups = [[part] for part in parts] if separate_items else [parts]
for index, group in enumerate(groups):
item = {
"id": f"msg_piece_{index}",
"type": "message",
"role": "assistant",
"status": "completed",
"content": [
{"type": "output_text", "text": text, "annotations": []}
for text in group
],
}
items.append(item)
events.append(
{
"type": "response.output_item.added",
"output_index": index,
"item": {
**item,
"content": item["content"]
if representation == "item_added"
else [],
"status": "in_progress",
},
}
)
for position, part in enumerate(item["content"]):
identity = {
"output_index": index,
"item_id": item["id"],
"content_index": position,
}
events.append(
{
"type": "response.content_part.added",
**identity,
"part": {
**part,
"text": part["text"]
if representation in {"part_added", "item_added"}
else part["text"][:1]
if representation == "mixed"
else "",
},
}
)
if representation in {"deltas", "mixed"}:
events.extend(
{
"type": "response.output_text.delta",
**identity,
"delta": character,
}
for character in (
part["text"][1:] if representation == "mixed" else part["text"]
)
)
if representation not in {"item_done", "part_done", "terminal"}:
events.append(
{
"type": "response.output_text.done",
**identity,
"text": part["text"],
}
)
if representation not in {"item_done", "terminal"}:
events.append(
{"type": "response.content_part.done", **identity, "part": part}
)
events.append(
{"type": "response.output_item.done", "output_index": index, "item": item}
)
terminal = deepcopy(template[-1])
terminal["response"]["output"] = items
events.append(terminal)
if representation == "terminal":
events = [events[0], terminal]
return [
{**deepcopy(event), "sequence_number": number}
for number, event in enumerate(events)
]
def response_text(event):
return "".join(
part.get("text", "")
for item in event.data["response"]["output"]
for part in item.get("content", [])
)
def assert_completion_text_agrees(events):
final = {
item["id"]: item
for item in events[-1].data["response"]["output"]
if item["type"] == "message"
}
for event in events:
data = event.data
if event.event == "response.output_text.done":
part = final[data["item_id"]]["content"][data["content_index"]]
assert data["text"] == part["text"]
elif event.event == "response.content_part.done":
part = final[data["item_id"]]["content"][data["content_index"]]
assert data["part"] == part
elif event.event == "response.output_item.done":
assert data["item"]["content"] == final[data["item"]["id"]]["content"]
numbers = [event.data["sequence_number"] for event in events]
assert all(before < after for before, after in pairwise(numbers)), numbers
@pytest.mark.asyncio
@pytest.mark.parametrize("separate_items", [False, True])
@pytest.mark.parametrize(
"representation",
[
"terminal",
"item_done",
"part_done",
"text_done",
"deltas",
"part_added",
"item_added",
"mixed",
],
)
async def test_split_replay_has_one_answer_across_representations(
separate_items, representation
):
def reply(bodies):
if len(bodies) == 1:
return 200, text_events("responses", "Hello ", complete=False)
return 200, responses_parts(
["He", "llo world."],
separate_items=separate_items,
representation=representation,
)
async with _harness("responses", reply, max_attempts=2) as (send, bodies, _):
raw = await delivered(
send("responses", [{"role": "user", "content": "greet"}]), "responses"
)
events = parse_sse_text(raw)
assert len(bodies) == 2
assert events[-1].event == "response.completed"
assert response_text(events[-1]) == "Hello world."
retained = [
part["text"]
for item in events[-1].data["response"]["output"][1:]
for part in item["content"]
]
assert retained == ["", "world."]
assert_completion_text_agrees(events)
@pytest.mark.asyncio
@pytest.mark.parametrize("representation", ["terminal", "text_done", "mixed"])
async def test_split_pure_replay_is_not_public_progress(representation):
def reply(bodies):
if len(bodies) == 1:
return 200, text_events("responses", "Hello ", complete=False)
return 200, responses_parts(["He", "llo "], representation=representation)
async with _harness("responses", reply, max_attempts=3) as (send, bodies, _):
raw = await delivered(
send("responses", [{"role": "user", "content": "greet"}]), "responses"
)
events = parse_sse_text(raw)
assert len(bodies) == 2
assert events[-1].event == "response.failed"
assert response_text(events[-1]) == "Hello "
@pytest.mark.asyncio
@pytest.mark.parametrize("wire", ["messages", "responses"])
@pytest.mark.parametrize("seeded", [False, True])
async def test_messages_replay_across_blocks_uses_the_same_rule(wire, seeded):
def reply(bodies):
if len(bodies) == 1:
return 200, text_events("messages", "abcabc", complete=False)
template = text_events("messages", "")
events = [template[0]]
for index, text in enumerate(["abc", "abcX"]):
part = text_events("messages", text)[1:-2]
for event in part:
event["index"] = index
if seeded:
part[0]["content_block"]["text"] = text
part.pop(1)
events.extend(part)
return 200, [*events, *template[-2:]]
async with _harness("messages", reply, max_attempts=2) as (send, bodies, _):
raw = await delivered(
send(wire, [{"role": "user", "content": "continue"}]), wire
)
events = parse_sse_text(raw)
assert len(bodies) == 2
assert events[-1].event == (
"message_stop" if wire == "messages" else "response.completed"
)
assert public_text(events, wire) == "abcabcX"
if wire == "responses":
assert response_text(events[-1]) == "abcabcX"
assert_completion_text_agrees(events)
@pytest.mark.asyncio
async def test_overflow_preserves_public_prefix_and_closes_the_source(monkeypatch):
monkeypatch.setattr(
"free_claude_code.core.continuation_overlap.RECOVERY_RETENTION_BYTES", 1000
)
closed = []
attempts = []
async def source():
state = current_stream_delivery()
assert state is not None
try:
state.begin_attempt()
attempts.append(1)
for frame in _frames(
"responses",
[
(event["type"], event)
for event in text_events("responses", "Hello ", complete=False)
],
):
yield frame
state.begin_continuation()
state.begin_attempt()
attempts.append(2)
for event in responses_parts(["He"], representation="text_done")[:4]:
yield _frames("responses", [(event["type"], event)])[0]
for _ in range(30):
event = {"type": "ping", "padding": "x" * 60}
yield _frames("responses", [(event["type"], event)])[0]
pytest.fail("the owner must stop reading after its overlap limit")
finally:
closed.append(True)
raw = await delivered(source(), "responses")
events = parse_sse_text(raw)
assert attempts == [1, 2]
assert closed == [True]
assert events[-1].event == "response.failed"
assert response_text(events[-1]) == "Hello "
assert sum(event.event == "response.failed" for event in events) == 1
assert not any(event.event == "response.completed" for event in events)
@pytest.mark.asyncio
async def test_overlap_release_records_each_frame_only_when_returned():
states = []
resumed = []
async def source():
state = current_stream_delivery()
assert state is not None
states.append(state)
for frame in _frames(
"responses",
[
(event["type"], event)
for event in text_events("responses", "Hello ", complete=False)
],
):
yield frame
state.begin_continuation()
for event in responses_parts(["He", "llo world."], representation="text_done"):
yield _frames("responses", [(event["type"], event)])[0]
prefix = state.prefix
assert prefix is not None
if (
event["type"] == "response.output_text.done"
and event["content_index"] == 0
):
assert prefix.text == "Hello "
if (
event["type"] == "response.output_text.done"
and event["content_index"] == 1
):
resumed.append(prefix.text)
response = await _response("responses", source())
events = []
try:
async for chunk in response.body_iterator:
frames = parse_sse_text(str(chunk))
assert len(frames) == 1
event = frames[0]
events.append(event)
if (
event.event == "response.output_text.done"
and event.data["text"] == "world."
):
assert states[0].prefix.text == "Hello world."
assert resumed == []
assert resumed == ["Hello world."]
assert_completion_text_agrees(events)
finally:
await response.aclose()
@pytest.mark.asyncio
async def test_hidden_attempt_replacement_discards_queued_replay():
unpublished = []
async def source():
state = current_stream_delivery()
assert state is not None
state.begin_attempt()
prefix = text_events("responses", "Hello ", complete=False)
for frame in _frames("responses", [(event["type"], event) for event in prefix]):
yield frame
state.begin_continuation()
for _ in range(2):
state.begin_attempt()
event = {
"type": "response.output_text.delta",
"output_index": 0,
"content_index": 0,
"item_id": "msg_abandoned",
"delta": "He",
}
yield _frames("responses", [(event["type"], event)])[0]
prefix = state.prefix
assert prefix is not None
unpublished.append((state.attempt_content_released, prefix.text))
state.begin_attempt()
for event in responses_parts(["world."], representation="text_done"):
yield _frames("responses", [(event["type"], event)])[0]
raw = await delivered(source(), "responses")
assert unpublished == [(False, "Hello "), (False, "Hello ")]
events = parse_sse_text(raw)
assert events[-1].event == "response.completed"
assert response_text(events[-1]) == "Hello world."
assert "msg_abandoned" not in raw
assert_completion_text_agrees(events)
@pytest.mark.asyncio
async def test_cancellation_drops_ambiguous_replay_and_closes_the_source():
waiting = asyncio.Event()
closed = []
states = []
async def source():
state = current_stream_delivery()
assert state is not None
states.append(state)
try:
prefix = text_events("responses", "Hello ", complete=False)
for frame in _frames(
"responses", [(event["type"], event) for event in prefix]
):
yield frame
state.begin_continuation()
for event in responses_parts(["He"], representation="text_done")[:4]:
yield _frames("responses", [(event["type"], event)])[0]
waiting.set()
await asyncio.Event().wait()
finally:
closed.append(True)
response = await _response("responses", source())
returned = []
async def consume():
async for chunk in response.body_iterator:
returned.extend(parse_sse_text(str(chunk)))
reader = asyncio.create_task(consume())
try:
await asyncio.wait_for(waiting.wait(), 1)
assert states[0].prefix.text == "Hello "
reader.cancel()
with pytest.raises(asyncio.CancelledError):
await reader
finally:
reader.cancel()
await asyncio.gather(reader, return_exceptions=True)
await response.aclose()
assert closed == [True]
assert not any(
event.event in {"response.failed", "response.completed"} for event in returned
)
assert all(
event.data["text"] == "Hello "
for event in returned
if event.event == "response.output_text.done"
)
@pytest.mark.asyncio
@pytest.mark.parametrize("representation", ["text_done", "part_done", "item_done"])
async def test_snapshot_text_is_published_once_before_another_interruption(
representation,
):
def reply(bodies):
if len(bodies) == 1:
return 200, text_events("responses", "Hello ", complete=False)
if len(bodies) == 2:
return 200, responses_parts(
["Hello world."], representation=representation
)[:-1]
return 200, text_events("responses", " Finished.")
async with _harness("responses", reply, max_attempts=3) as (send, bodies, _):
raw = await delivered(
send("responses", [{"role": "user", "content": "greet"}]), "responses"
)
assert len(bodies) == 3
assert bodies[2]["input"][-2]["content"] == [
{"type": "output_text", "text": "Hello world."}
]
events = parse_sse_text(raw)
assert events[-1].event == "response.completed"
assert response_text(events[-1]) == "Hello world. Finished."
assert_completion_text_agrees(events)

View file

@ -0,0 +1,834 @@
"""Recovery extends one public response after content has been delivered."""
import asyncio
import json
from copy import deepcopy
from itertools import pairwise
import httpx
import pytest
from fastapi.testclient import TestClient
from free_claude_code.config.settings import Settings
from free_claude_code.core.anthropic.passthrough import NativeMessagesRequest
from free_claude_code.core.anthropic.stream_contracts import parse_sse_text
from free_claude_code.providers.failure_policy import RetryableProviderProtocolError
from free_claude_code.providers.request_recovery import RequestCorrections
from free_claude_code.providers.stream_recovery import RecoveryHoldbackBuffer
from tests.api.support import create_test_app, provider_manager_for_app
from tests.api.test_hidden_stream_retries import delivered, partial_tool
from tests.api.test_tool_call_buffer import _response
from tests.api.test_tool_call_buffer_transports import _tools
from tests.providers.test_anthropic_messages_transport import Wire, _sse
from tests.providers.test_anthropic_provider import native_body, provider
from tests.providers.test_history_transports import _events_for, _harness
from tests.providers.test_native_tool_arguments import tool_events
@pytest.fixture(autouse=True)
def release_provider_events(monkeypatch):
monkeypatch.setattr(
"free_claude_code.providers.stream_recovery.RecoveryHoldbackBuffer",
lambda: RecoveryHoldbackBuffer(max_bytes=1),
)
def text_events(protocol, text, *, complete=True):
if protocol == "chat":
template = _events_for(protocol)[0]
events = [
{
**template,
"choices": [
{"index": 0, "delta": {"content": text}, "finish_reason": None}
],
}
]
if complete:
events.append(
{
**template,
"choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
}
)
return events
if protocol == "messages":
events = [
_events_for(protocol)[0],
{
"type": "content_block_start",
"index": 0,
"content_block": {"type": "text", "text": ""},
},
{
"type": "content_block_delta",
"index": 0,
"delta": {"type": "text_delta", "text": text},
},
]
if complete:
events.extend(
[
{"type": "content_block_stop", "index": 0},
{
"type": "message_delta",
"delta": {"stop_reason": "end_turn"},
"usage": {"output_tokens": 2},
},
{"type": "message_stop"},
]
)
return events
response = deepcopy(_events_for(protocol)[-1]["response"])
item = {
"id": "msg_text",
"type": "message",
"role": "assistant",
"status": "completed",
"content": [{"type": "output_text", "text": text, "annotations": []}],
}
response["output"] = [item]
events = [
{
"type": "response.created",
"response": {**response, "status": "in_progress", "output": []},
},
{
"type": "response.output_item.added",
"output_index": 0,
"item": {**item, "status": "in_progress", "content": []},
},
{
"type": "response.content_part.added",
"output_index": 0,
"item_id": "msg_text",
"content_index": 0,
"part": {"type": "output_text", "text": "", "annotations": []},
},
{
"type": "response.output_text.delta",
"output_index": 0,
"item_id": "msg_text",
"content_index": 0,
"delta": text,
},
]
if complete:
events.extend(
[
{
"type": "response.output_text.done",
"output_index": 0,
"item_id": "msg_text",
"content_index": 0,
"text": text,
},
{
"type": "response.content_part.done",
"output_index": 0,
"item_id": "msg_text",
"content_index": 0,
"part": item["content"][0],
},
{"type": "response.output_item.done", "output_index": 0, "item": item},
{"type": "response.completed", "response": response},
]
)
return [{**event, "sequence_number": index} for index, event in enumerate(events)]
def public_text(events, wire):
if wire == "messages":
return "".join(
event.data.get("delta", {}).get("text", "")
+ event.data.get("content_block", {}).get("text", "")
for event in events
)
return "".join(
event.data["delta"]
for event in events
if event.event == "response.output_text.delta"
)
def assert_completed(events, wire):
assert events[-1].event == (
"message_stop" if wire == "messages" else "response.completed"
)
assert (
sum(
event.event
== ("message_start" if wire == "messages" else "response.created")
for event in events
)
== 1
)
assert not any(event.event in {"error", "response.failed"} for event in events)
if wire == "responses":
numbers = [event.data["sequence_number"] for event in events]
assert all(right > left for left, right in pairwise(numbers))
assert (
len(
{
event.data["response"]["id"]
for event in events
if "response" in event.data
}
)
== 1
)
@pytest.mark.asyncio
@pytest.mark.parametrize("protocol", ["chat", "messages", "responses"])
@pytest.mark.parametrize("wire", ["messages", "responses"])
async def test_text_cutoff_continues_one_response_and_preserves_tools(protocol, wire):
def reply(bodies):
return 200, text_events(
protocol,
"Hello " if len(bodies) == 1 else "world.",
complete=len(bodies) > 1,
)
async with _harness(protocol, reply, max_attempts=2) as (send, bodies, _):
result = await delivered(
send(wire, [{"role": "user", "content": "greet"}], tools=_tools(wire)), wire
)
events = parse_sse_text(result)
assert_completed(events, wire)
assert public_text(events, wire) == "Hello world."
assert len(bodies) == 2
assert bodies[1]["model"] == bodies[0]["model"]
assert bodies[1]["tools"] == bodies[0]["tools"]
assert "Hello " in str(bodies[1])
if wire == "responses":
output = events[-1].data["response"]["output"]
assert (
"".join(
part.get("text", "")
for item in output
for part in item.get("content", [])
)
== "Hello world."
)
@pytest.mark.asyncio
@pytest.mark.parametrize("protocol", ["messages", "responses"])
@pytest.mark.parametrize("wire", ["messages", "responses"])
async def test_released_tool_finishes_handoff_without_another_request(protocol, wire):
def reply(_bodies):
events = tool_events(protocol, '{"path":"kept"}')
return 200, events[:-2] if protocol == "messages" else events[:-1]
async with _harness(protocol, reply, max_attempts=1) as (send, bodies, _):
result = await delivered(
send(wire, [{"role": "user", "content": "read"}], tools=_tools(wire)), wire
)
events = parse_sse_text(result)
assert len(bodies) == 1
assert_completed(events, wire)
if wire == "messages":
assert events[-2].data["delta"]["stop_reason"] == "tool_use"
else:
assert events[-1].data["response"]["output"][0]["call_id"] == "call_probe"
def after_text(protocol, tail):
events = text_events(protocol, "Before. ")
events = events[:-1] if protocol != "messages" else events[:-2]
tail = deepcopy(tail if protocol == "chat" else tail[1:])
for event in tail:
field = "index" if protocol == "messages" else "output_index"
if field in event:
event[field] += 1
combined = events + tail
if protocol == "responses":
for index, event in enumerate(combined):
event["sequence_number"] = index
return combined
@pytest.mark.asyncio
@pytest.mark.parametrize("protocol", ["chat", "messages", "responses"])
@pytest.mark.parametrize("wire", ["messages", "responses"])
async def test_partial_call_after_public_text_is_discarded_before_continuation(
protocol, wire
):
def reply(bodies):
if len(bodies) == 1:
return 200, after_text(protocol, partial_tool(protocol))
assert "abandoned" not in str(bodies[-1])
return 200, tool_events(protocol, '{"path":"winning"}')
async with _harness(protocol, reply, max_attempts=2) as (send, bodies, _):
result = await delivered(
send(wire, [{"role": "user", "content": "read"}], tools=_tools(wire)), wire
)
events = parse_sse_text(result)
assert_completed(events, wire)
assert len(bodies) == 2
assert "call_abandoned" not in result
assert public_text(events, wire) == "Before. "
if wire == "messages":
arguments = "".join(
e.data.get("delta", {}).get("partial_json", "") for e in events
)
assert arguments == '{"path":"winning"}'
else:
assert "winning" in result
calls = [
item
for item in events[-1].data["response"]["output"]
if item["type"] == "function_call"
]
assert len(calls) == 1
assert calls[0]["arguments"] == '{"path":"winning"}'
@pytest.mark.asyncio
@pytest.mark.parametrize("protocol", ["chat", "messages", "responses"])
@pytest.mark.parametrize("wire", ["messages", "responses"])
async def test_repeated_interruption_uses_latest_prefix_and_one_budget(protocol, wire):
def reply(bodies):
index = len(bodies) - 1
return 200, text_events(
protocol, ["One ", "two ", "three."][index], complete=index == 2
)
async with _harness(protocol, reply, max_attempts=3) as (send, bodies, _):
result = await delivered(
send(wire, [{"role": "user", "content": "count"}]), wire
)
events = parse_sse_text(result)
assert_completed(events, wire)
assert public_text(events, wire) == "One two three."
assert len(bodies) == 3
history = bodies[-1].get("messages", bodies[-1].get("input"))
assert len(history) == 3
assert "One two " in str(history)
@pytest.mark.asyncio
@pytest.mark.parametrize("protocol", ["chat", "messages", "responses"])
@pytest.mark.parametrize("wire", ["messages", "responses"])
@pytest.mark.parametrize(
"candidate,expected",
[
("Hello world.", "Hello world."),
("Hel", "Hello Hel"),
(" world.", "Hello world."),
],
)
async def test_exact_overlap_filter_preserves_nonmatching_text(
protocol, wire, candidate, expected
):
def reply(bodies):
return 200, text_events(
protocol,
"Hello " if len(bodies) == 1 else candidate,
complete=len(bodies) > 1,
)
async with _harness(protocol, reply, max_attempts=2) as (send, _, _):
result = await delivered(
send(wire, [{"role": "user", "content": "greet"}]), wire
)
events = parse_sse_text(result)
assert_completed(events, wire)
assert public_text(events, wire) == expected
if wire == "responses":
output = events[-1].data["response"]["output"]
assert (
"".join(
part.get("text", "")
for item in output
for part in item.get("content", [])
)
== expected
)
@pytest.mark.asyncio
@pytest.mark.parametrize("protocol", ["chat", "messages", "responses"])
@pytest.mark.parametrize("wire", ["messages", "responses"])
async def test_exact_replay_without_new_output_is_not_success(protocol, wire):
async with _harness(
protocol,
lambda bodies: (200, text_events(protocol, "Same.", complete=len(bodies) > 1)),
max_attempts=2,
) as (send, bodies, _):
result = await delivered(
send(wire, [{"role": "user", "content": "greet"}]), wire
)
events = parse_sse_text(result)
assert len(bodies) == 2
assert public_text(events, wire) == "Same."
assert events[-1].event == ("error" if wire == "messages" else "response.failed")
@pytest.mark.asyncio
@pytest.mark.parametrize("protocol", ["chat", "messages", "responses"])
@pytest.mark.parametrize("wire", ["messages", "responses"])
@pytest.mark.parametrize(
"prefix,candidate,expected",
[
("The answer is", "answer is 42", "The answer is 42"),
("abcabc", "abcabcX", "abcabcX"),
("abcabd", "abcX", "abcabdabcX"),
("very ", "very good", "very good"),
("Hello 世界", "世界!", "Hello 世界!"),
],
)
async def test_partial_overlap_across_single_character_chunks(
protocol, wire, prefix, candidate, expected
):
def reply(bodies):
if len(bodies) == 1:
return 200, text_events(protocol, prefix, complete=False)
events = text_events(protocol, candidate)
expanded = []
for event in events:
if protocol == "chat" and event["choices"][0]["delta"].get("content"):
for character in candidate:
chunk = deepcopy(event)
chunk["choices"][0]["delta"]["content"] = character
expanded.append(chunk)
elif event.get("type") == "content_block_delta":
expanded.extend(
{**event, "delta": {"type": "text_delta", "text": character}}
for character in candidate
)
elif event.get("type") == "response.output_text.delta":
expanded.extend(
{**event, "delta": character} for character in candidate
)
else:
expanded.append(event)
return 200, expanded
async with _harness(protocol, reply, max_attempts=2) as (send, _, _):
result = await delivered(
send(wire, [{"role": "user", "content": "continue"}]), wire
)
events = parse_sse_text(result)
assert_completed(events, wire)
assert public_text(events, wire) == expected
if wire == "responses":
assert (
"".join(
part.get("text", "")
for item in events[-1].data["response"]["output"]
for part in item.get("content", [])
)
== expected
)
@pytest.mark.asyncio
async def test_native_passthrough_continuation_closes_original_connection():
bodies = []
first = Wire([_sse(*text_events("messages", "Hello ", complete=False))])
second = Wire([_sse(*text_events("messages", "world."))])
def reply(request):
bodies.append(json.loads(request.content))
if len(bodies) == 2:
assert first.closed
return httpx.Response(
200,
headers={"content-type": "text/event-stream"},
stream=first if len(bodies) == 1 else second,
)
p = provider(reply)
try:
result = await delivered(
p.stream_native_messages(NativeMessagesRequest(native_body(True))),
"messages",
)
events = parse_sse_text(result)
assert_completed(events, "messages")
assert public_text(events, "messages") == "Hello world."
assert len(bodies) == 2
assert first.closed and second.closed
finally:
await p.cleanup()
@pytest.mark.asyncio
@pytest.mark.parametrize("wire", ["messages", "responses"])
@pytest.mark.parametrize("complete", [False, True])
async def test_retention_limit_does_not_truncate_healthy_output(
monkeypatch, wire, complete
):
monkeypatch.setattr(
"free_claude_code.core.delivered_response.RECOVERY_RETENTION_BYTES", 100
)
async with _harness(
"chat", lambda _: (200, text_events("chat", "x" * 1000, complete=complete))
) as (send, bodies, _):
result = await delivered(
send(wire, [{"role": "user", "content": "write"}]), wire
)
events = parse_sse_text(result)
assert len(bodies) == 1
assert public_text(events, wire) == "x" * 1000
if complete:
assert_completed(events, wire)
else:
assert events[-1].event == (
"error" if wire == "messages" else "response.failed"
)
@pytest.mark.asyncio
@pytest.mark.parametrize("wire", ["messages", "responses"])
@pytest.mark.parametrize("continuation", [False, True])
async def test_stopped_continuation_cannot_request_another_body_correction(
monkeypatch, wire, continuation
):
corrections = []
def correction(*args, **kwargs):
corrections.append(True)
return None
monkeypatch.setattr(RequestCorrections, "next_body", correction)
def adapter(kind, payload):
if kind == "response.completed":
raise RetryableProviderProtocolError("malformed terminal snapshot")
return payload
async with _harness(
"responses",
lambda bodies: (
200,
text_events(
"responses",
"First " if len(bodies) == 1 else "second",
complete=not continuation or len(bodies) > 1,
),
),
) as (send, bodies, transport):
transport._event_adapter_factory = lambda: adapter
result = await delivered(
send(wire, [{"role": "user", "content": "write"}]), wire
)
assert len(bodies) == (2 if continuation else 1)
assert corrections == []
assert parse_sse_text(result)[-1].event == (
"error" if wire == "messages" else "response.failed"
)
@pytest.mark.asyncio
@pytest.mark.parametrize("protocol", ["chat", "messages", "responses"])
@pytest.mark.parametrize("wire", ["messages", "responses"])
async def test_opaque_reasoning_is_not_regenerated_after_delivery(protocol, wire):
events = _events_for(protocol)
if protocol == "chat":
events = events[:1]
elif protocol == "messages":
events = events[:-2]
else:
events = events[:-1]
async with _harness(protocol, lambda _: (200, events)) as (send, bodies, _):
result = await delivered(
send(wire, [{"role": "user", "content": "think"}]), wire
)
assert len(bodies) == 1
assert parse_sse_text(result)[-1].event == (
"error" if wire == "messages" else "response.failed"
)
@pytest.mark.asyncio
@pytest.mark.parametrize("wire", ["messages", "responses"])
async def test_hidden_hosted_action_after_text_blocks_continuation(wire):
events = after_text("responses", partial_tool("responses"))
events.append(
{
"type": "response.output_item.added",
"sequence_number": 40,
"output_index": 2,
"item": {
"type": "web_search_call",
"id": "ws_held",
"status": "in_progress",
},
}
)
async with _harness("responses", lambda _: (200, events)) as (send, bodies, _):
result = await delivered(
send(wire, [{"role": "user", "content": "search"}], tools=_tools(wire)),
wire,
)
assert len(bodies) == 1
assert "ws_held" not in result
assert parse_sse_text(result)[-1].event == (
"error" if wire == "messages" else "response.failed"
)
@pytest.mark.asyncio
async def test_cancellation_during_continuation_closes_both_attempts():
entered = asyncio.Event()
calls = []
first = Wire([_sse(*text_events("messages", "First ", complete=False))])
class BlockingWire(httpx.AsyncByteStream):
closes = 0
async def __aiter__(self):
yield _sse(*text_events("messages", "second ", complete=False))
entered.set()
await asyncio.Event().wait()
async def aclose(self):
self.closes += 1
second = BlockingWire()
def reply(request):
calls.append(request)
return httpx.Response(
200,
headers={"content-type": "text/event-stream"},
stream=first if len(calls) == 1 else second,
)
p = provider(reply)
response = await _response(
"messages", p.stream_native_messages(NativeMessagesRequest(native_body(True)))
)
try:
async def read():
return [chunk async for chunk in response.body_iterator]
task = asyncio.create_task(read())
await asyncio.wait_for(entered.wait(), 2)
task.cancel()
with pytest.raises(asyncio.CancelledError):
await task
await response.aclose()
await response.aclose()
assert len(calls) == 2
assert first.closed and second.closes == 1
finally:
await p.cleanup()
@pytest.mark.asyncio
@pytest.mark.parametrize("wire", ["messages", "responses"])
async def test_completed_signed_thinking_and_call_can_finish_without_regeneration(wire):
events = _events_for("messages")[:-2]
calls = tool_events("messages", '{"path":"kept"}')[1:-2]
for event in calls:
if "index" in event:
event["index"] += 1
async with _harness(
"messages", lambda _: (200, events + calls), max_attempts=1
) as (send, bodies, _):
result = await delivered(
send(wire, [{"role": "user", "content": "read"}], tools=_tools(wire)), wire
)
assert len(bodies) == 1
assert_completed(parse_sse_text(result), wire)
def test_http_recovery_then_real_harness_tool_result_has_no_private_history():
sent = []
wires = []
def reply(request):
body = json.loads(request.content)
sent.append(body)
if len(sent) == 3:
return httpx.Response(
200,
json={
"type": "message",
"model": "primary",
"content": [{"type": "text", "text": "result received"}],
},
)
events = (
after_text("messages", partial_tool("messages"))
if len(sent) == 1
else tool_events("messages", '{"path":"winning"}')
)
wire = Wire([_sse(*events)])
wires.append(wire)
return httpx.Response(
200, headers={"content-type": "text/event-stream"}, stream=wire
)
app = create_test_app(
Settings(MODEL="anthropic/primary", ENABLE_WEB_SERVER_TOOLS=False),
providers={"anthropic": provider(reply)},
)
body = {
**native_body(True),
"model": "anthropic/primary",
"tools": _tools("messages"),
}
with TestClient(app) as client:
first = client.post("/v1/messages", json=body)
assert first.status_code == 200
events = parse_sse_text(first.text)
assert_completed(events, "messages")
calls = [
event.data["content_block"]
for event in events
if event.event == "content_block_start"
and event.data["content_block"]["type"] == "tool_use"
]
assert len(calls) == 1
call = {**calls[0], "input": {"path": "winning"}}
history = [
*body["messages"],
{
"role": "assistant",
"content": [{"type": "text", "text": "Before. "}, call],
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": call["id"],
"content": "actual file contents",
}
],
},
]
second = client.post(
"/v1/messages", json={**body, "stream": False, "messages": history}
)
assert second.status_code == 200
assert second.json()["content"][0]["text"] == "result received"
assert len(sent) == 3
assert sent[2]["messages"] == history
assert "previous provider stream" not in str(sent[2])
assert all(wire.closed for wire in wires)
assert provider_manager_for_app(app)._current.active_leases == 0
@pytest.mark.asyncio
@pytest.mark.parametrize("wire", ["messages", "responses"])
async def test_unsigned_thinking_can_continue_into_text(wire):
first = text_events("chat", "", complete=False)
first[0]["choices"][0]["delta"] = {
"reasoning_content": "Look at the request first."
}
async with _harness(
"chat",
lambda bodies: (
200,
first if len(bodies) == 1 else text_events("chat", "Answer."),
),
max_attempts=2,
) as (send, bodies, _):
result = await delivered(
send(wire, [{"role": "user", "content": "answer"}]), wire
)
events = parse_sse_text(result)
assert_completed(events, wire)
assert public_text(events, wire) == "Answer."
assert "Look at the request first." in str(bodies[-1])
@pytest.mark.asyncio
@pytest.mark.parametrize("wire", ["messages", "responses"])
@pytest.mark.parametrize("echo", [False, True])
async def test_retention_limit_during_continuation_keeps_final_output_consistent(
monkeypatch, wire, echo
):
monkeypatch.setattr(
"free_claude_code.core.delivered_response.RECOVERY_RETENTION_BYTES", 1200
)
async with _harness(
"responses",
lambda bodies: (
200,
text_events(
"responses",
"First "
if len(bodies) == 1
else ("First " if echo else "") + "x" * 2000,
complete=len(bodies) > 1,
),
),
max_attempts=2,
) as (send, bodies, _):
result = await delivered(
send(wire, [{"role": "user", "content": "write"}]), wire
)
events = parse_sse_text(result)
assert_completed(events, wire)
assert public_text(events, wire) == "First " + "x" * 2000
assert len(bodies) == 2
if wire == "responses":
final = events[-1].data["response"]
assert (
"".join(
part.get("text", "")
for item in final["output"]
for part in item.get("content", [])
)
== "First " + "x" * 2000
)
streamed_ids = [
event.data["item"]["id"]
for event in events
if event.event == "response.output_item.done"
]
assert [item["id"] for item in final["output"]] == streamed_ids
assert final["usage"]["output_tokens"] > 0
else:
assert events[-2].data["usage"]["output_tokens"] >= 250
@pytest.mark.asyncio
@pytest.mark.parametrize("wire", ["messages", "responses"])
async def test_partial_replay_without_new_output_is_not_success(wire):
async with _harness(
"messages",
lambda bodies: (
200,
text_events(
"messages",
"The answer is" if len(bodies) == 1 else "answer is",
complete=len(bodies) > 1,
),
),
max_attempts=2,
) as (send, _, _):
result = await delivered(
send(wire, [{"role": "user", "content": "continue"}]), wire
)
events = parse_sse_text(result)
assert public_text(events, wire) == "The answer is"
assert events[-1].event == ("error" if wire == "messages" else "response.failed")
@pytest.mark.asyncio
@pytest.mark.parametrize("wire", ["messages", "responses"])
async def test_seeded_text_start_participates_in_overlap_filter(wire):
def reply(bodies):
if len(bodies) == 1:
return 200, text_events("messages", "The answer is", complete=False)
events = text_events("messages", " 42")
events[1]["content_block"]["text"] = "answer is"
return 200, events
async with _harness("messages", reply, max_attempts=2) as (send, _, _):
result = await delivered(
send(wire, [{"role": "user", "content": "continue"}]), wire
)
events = parse_sse_text(result)
assert_completed(events, wire)
assert public_text(events, wire) == "The answer is 42"

View file

@ -32,7 +32,9 @@ from tests.providers.test_request_recovery_transitions import Wire
async def _response(source, wire):
options = {"pre_start_error_response": _json_error, "request_id": "stopped"}
return await (
anthropic_sse_streaming_response(source, **options)
anthropic_sse_streaming_response(
source, pre_start_error_response=_json_error, request_id="stopped"
)
if wire == "messages"
else openai_responses_sse_streaming_response(source, headers={}, **options)
)

View file

@ -0,0 +1,190 @@
"""A pending recovery retains public output even before its first new event."""
import asyncio
import json
from itertools import pairwise
import httpx
import httpx2
import pytest
from openai import AsyncOpenAI
from free_claude_code.application.execution import ProviderExecutor
from free_claude_code.core.anthropic.stream_contracts import parse_sse_text
from free_claude_code.core.stream_delivery import current_stream_delivery
from free_claude_code.providers.stream_recovery import RecoveryHoldbackBuffer
from tests.api.test_delivered_stream_recovery import public_text, text_events
from tests.api.test_hidden_stream_retries import delivered
from tests.api.test_stream_delivery import drained
from tests.api.test_tool_call_buffer import _response
from tests.providers.test_history_transports import _harness
from tests.providers.test_native_tool_arguments import tool_events
@pytest.mark.asyncio
@pytest.mark.parametrize("protocol", ["chat", "messages", "responses"])
@pytest.mark.parametrize("wire", ["messages", "responses"])
@pytest.mark.parametrize("metadata_first", [False, True])
async def test_continuation_timeout_retains_delivered_output(
monkeypatch, protocol, wire, metadata_first
):
module = httpx if protocol == "messages" else httpx2
native_transport = module.MockTransport
closed = []
class WaitingStream(httpx.AsyncByteStream, httpx2.AsyncByteStream):
async def __aiter__(self):
if metadata_first:
start = text_events(protocol, "")[0]
yield ("data: " + json.dumps(start) + "\n\n").encode()
await asyncio.Event().wait()
yield b""
async def aclose(self):
closed.append(True)
def transport(handler):
calls = 0
async def handle(request):
nonlocal calls
response = handler(request)
calls += 1
if calls > 1:
await response.aclose()
return module.Response(
200,
headers={"content-type": "text/event-stream"},
stream=WaitingStream(),
)
return response
return native_transport(handle)
def executor(*args, **kwargs):
kwargs["progress_timeout_seconds"] = 0.2
return ProviderExecutor(*args, **kwargs)
with monkeypatch.context() as patch:
patch.setattr(module, "MockTransport", transport)
patch.setattr("tests.api.test_hidden_stream_retries.ProviderExecutor", executor)
patch.setattr(
"free_claude_code.providers.stream_recovery.RecoveryHoldbackBuffer",
lambda: RecoveryHoldbackBuffer(max_bytes=1),
)
async with _harness(
protocol,
lambda _: (200, text_events(protocol, "Hello ", complete=False)),
max_attempts=3,
) as (send, bodies, _):
raw = await delivered(
send(wire, [{"role": "user", "content": "greet"}]), wire
)
events = parse_sse_text(raw)
assert len(bodies) == 2 and closed == [True]
assert public_text(events, wire) == "Hello "
failure_type = "error" if wire == "messages" else "response.failed"
assert events[-1].event == failure_type
assert sum(event.event == failure_type for event in events) == 1
assert not any(
event.event in {"message_stop", "response.completed"} for event in events
)
failure = events[-1].data if wire == "messages" else events[-1].data["response"]
assert failure["error"]["type"] == "timeout_error"
if wire == "messages":
return
assert failure["id"] == events[0].data["response"]["id"]
assert failure["status"] == "failed"
assert failure["usage"]["output_tokens"] > 0
assert all(
left.data["sequence_number"] < right.data["sequence_number"]
for left, right in pairwise(events)
)
assert (
"".join(
part.get("text", "")
for item in failure["output"]
for part in item.get("content") or []
)
== "Hello "
)
http = httpx2.AsyncClient(
transport=httpx2.MockTransport(
lambda _: httpx2.Response(
200, headers={"content-type": "text/event-stream"}, content=raw.encode()
)
)
)
async with (
AsyncOpenAI(api_key="fixture", max_retries=0, http_client=http) as client,
client.responses.stream(model="fixture", input="greet") as stream,
):
seen = [event async for event in stream]
assert seen[-1].type == "response.failed"
output = seen[-1].response.output[0]
assert output.type == "message"
part = output.content[0]
assert part.type == "output_text" and part.text == "Hello "
with pytest.raises(RuntimeError, match=r"response\.completed"):
await stream.get_final_response()
@pytest.mark.asyncio
@pytest.mark.parametrize("handoff", [False, True])
async def test_pending_recovery_failure_does_not_require_successful_closures(handoff):
prefix = (
tool_events("responses", '{"path":"x"}')[:-1]
if handoff
else text_events("responses", "Hello ", complete=False)
)
async def source():
for payload in prefix:
yield f"event: {payload['type']}\ndata: {json.dumps(payload)}\n\n"
state = current_stream_delivery()
assert state is not None
state.begin_continuation(handoff=handoff)
raise ValueError("recovery boundary failed")
events = parse_sse_text(await drained(source(), "responses"))
failure = events[-1].data["response"]
assert events[-1].event == "response.failed"
assert "recovery boundary failed" in failure["error"]["message"]
assert len(events) == len(prefix) + 1
assert len(failure["output"]) == 1
if handoff:
assert failure["output"][0] == prefix[-1]["item"]
else:
assert failure["output"][0]["status"] == "in_progress"
assert failure["output"][0]["content"][0]["text"] == "Hello "
@pytest.mark.asyncio
async def test_cancellation_during_pending_continuation_emits_no_failure():
closed = []
events = []
async def source():
try:
for payload in text_events("responses", "Hello ", complete=False):
yield f"event: {payload['type']}\ndata: {json.dumps(payload)}\n\n"
state = current_stream_delivery()
assert state is not None
state.begin_continuation()
raise asyncio.CancelledError
finally:
closed.append(True)
response = await _response("responses", source())
try:
with pytest.raises(asyncio.CancelledError):
async for chunk in response.body_iterator:
events.extend(parse_sse_text(str(chunk)))
finally:
await response.aclose()
assert public_text(events, "responses") == "Hello "
assert not any(event.event == "response.failed" for event in events)
assert closed == [True]

View file

@ -0,0 +1,261 @@
"""A continued response retains corrected requests and counts actual output."""
import json
from copy import deepcopy
import httpx
import pytest
from free_claude_code.core.anthropic.passthrough import NativeMessagesRequest
from free_claude_code.core.anthropic.stream_contracts import parse_sse_text
from free_claude_code.providers.stream_recovery import RecoveryHoldbackBuffer
from tests.api.test_delivered_stream_recovery import (
assert_completed,
public_text,
text_events,
)
from tests.api.test_hidden_stream_retries import delivered
from tests.providers.test_anthropic_messages_transport import Wire, _sse
from tests.providers.test_anthropic_provider import native_body, provider
from tests.providers.test_history_transports import _events_for, _harness, _saved_reply
@pytest.fixture(autouse=True)
def release_provider_events(monkeypatch):
monkeypatch.setattr(
"free_claude_code.providers.stream_recovery.RecoveryHoldbackBuffer",
lambda: RecoveryHoldbackBuffer(max_bytes=1),
)
@pytest.mark.asyncio
@pytest.mark.parametrize("protocol", ["chat", "messages", "responses"])
@pytest.mark.parametrize("wire", ["messages", "responses"])
async def test_empty_continuation_fails_without_compatibility_padding(protocol, wire):
def reply(bodies):
return 200, text_events(
protocol,
"Hello" if len(bodies) == 1 else "",
complete=len(bodies) > 1,
)
async with _harness(protocol, reply, max_attempts=3) as (send, bodies, _):
result = await delivered(
send(wire, [{"role": "user", "content": "greet"}]), wire
)
events = parse_sse_text(result)
assert len(bodies) == 2
assert public_text(events, wire) == "Hello"
assert events[-1].event == ("error" if wire == "messages" else "response.failed")
@pytest.mark.asyncio
@pytest.mark.parametrize("protocol", ["chat", "messages", "responses"])
@pytest.mark.parametrize("wire", ["messages", "responses"])
async def test_empty_initial_generation_preserves_compatibility_padding(protocol, wire):
async with _harness(protocol, lambda _: (200, text_events(protocol, ""))) as (
send,
bodies,
_,
):
result = await delivered(
send(wire, [{"role": "user", "content": "greet"}]), wire
)
events = parse_sse_text(result)
assert_completed(events, wire)
assert len(bodies) == 1
padded = protocol == "chat" or (protocol == "responses" and wire == "messages")
assert public_text(events, wire) == (" " if padded else "")
@pytest.mark.asyncio
@pytest.mark.parametrize("protocol", ["chat", "messages", "responses"])
@pytest.mark.parametrize("wire", ["messages", "responses"])
async def test_real_provider_whitespace_is_continuation_output(protocol, wire):
async with _harness(
protocol,
lambda bodies: (
200,
text_events(
protocol,
"Hello" if len(bodies) == 1 else " ",
complete=len(bodies) > 1,
),
),
max_attempts=2,
) as (send, bodies, _):
result = await delivered(
send(wire, [{"role": "user", "content": "greet"}]), wire
)
events = parse_sse_text(result)
assert_completed(events, wire)
assert public_text(events, wire) == "Hello "
assert len(bodies) == 2
@pytest.mark.asyncio
@pytest.mark.parametrize("wire", ["messages", "responses"])
async def test_readable_reasoning_continuation_succeeds_without_padding(wire):
reasoning = text_events("chat", "")
reasoning[0]["choices"][0]["delta"] = {
"reasoning_content": "Consider the greeting."
}
async with _harness(
"chat",
lambda bodies: (
200,
text_events("chat", "Hello", complete=False)
if len(bodies) == 1
else reasoning,
),
max_attempts=2,
) as (send, bodies, _):
result = await delivered(
send(wire, [{"role": "user", "content": "greet"}]), wire
)
events = parse_sse_text(result)
assert_completed(events, wire)
assert public_text(events, wire) == "Hello"
assert "Consider the greeting." in result
assert len(bodies) == 2
def rejected_history_error(protocol, body):
if protocol == "responses":
index = next(
i for i, item in enumerate(body["input"]) if item.get("encrypted_content")
)
param = f"input[{index}].encrypted_content"
else:
field = "content" if protocol == "messages" else "reasoning_details"
key = "signature" if protocol == "messages" else "data"
index, block_index = next(
(i, j)
for i, message in enumerate(body["messages"])
if isinstance(message.get(field), list)
for j, block in enumerate(message[field])
if block.get(key) == "opaque-original"
)
param = f"messages[{index}].{field}[{block_index}].{key}"
return {
"type": "invalid_request_error",
"code": "invalid_signature"
if protocol == "messages"
else "invalid_encrypted_content",
"param": param,
"message": "Historical reasoning record expired.",
}
@pytest.mark.asyncio
@pytest.mark.parametrize("protocol", ["chat", "messages", "responses"])
@pytest.mark.parametrize("wire", ["messages", "responses"])
async def test_history_correction_survives_another_interruption(protocol, wire):
def reply(bodies):
if len(bodies) == 1:
return 200, _events_for(protocol)
if len(bodies) > 2 and "opaque-original" in json.dumps(bodies[-1]):
return 400, rejected_history_error(protocol, bodies[-1])
return 200, text_events(
protocol,
"Hello "
if len(bodies) == 2
else "world "
if len(bodies) == 4
else "again.",
complete=len(bodies) == 5,
)
async with _harness(protocol, reply, max_attempts=4) as (send, bodies, _):
saved = await _saved_reply(
send(wire, [{"role": "user", "content": "think"}]), wire
)
history = [*saved, {"role": "user", "content": "greet"}]
original = deepcopy(history)
result = await delivered(send(wire, history), wire)
assert len(bodies) == 5
assert history == original
assert "opaque-original" in json.dumps(bodies[2])
assert all("opaque-original" not in json.dumps(body) for body in bodies[3:])
history_field = "input" if protocol == "responses" else "messages"
assert bodies[4][history_field][:-2] == bodies[3][history_field][:-2]
assert "Hello world " in str(bodies[4][history_field][-2])
events = parse_sse_text(result)
assert_completed(events, wire)
assert public_text(events, wire) == "Hello world again."
@pytest.mark.asyncio
async def test_removed_history_row_stays_removed_after_another_interruption():
history = [
{
"type": "reasoning",
"id": "rs_old",
"encrypted_content": "opaque-original",
"summary": [],
},
{"role": "user", "content": "greet"},
]
def reply(bodies):
if len(bodies) > 1 and "opaque-original" in json.dumps(bodies[-1]):
return 400, rejected_history_error("responses", bodies[-1])
return 200, text_events(
"responses",
"Hello "
if len(bodies) == 1
else "world "
if len(bodies) == 3
else "again.",
complete=len(bodies) == 4,
)
async with _harness("responses", reply, max_attempts=4) as (send, bodies, _):
result = await delivered(send("responses", history), "responses")
assert len(bodies) == 4
assert len(bodies[2]["input"]) == len(bodies[1]["input"]) - 1
assert bodies[3]["input"][:-2] == bodies[2]["input"][:-2]
assert_completed(parse_sse_text(result), "responses")
assert public_text(parse_sse_text(result), "responses") == "Hello world again."
@pytest.mark.asyncio
async def test_native_messages_repeated_continuation_replaces_only_private_tail():
bodies = []
wires = [
Wire([_sse(*text_events("messages", text, complete=index == 2))])
for index, text in enumerate(["Hello ", "world ", "again."])
]
def reply(request):
assert all(wire.closed for wire in wires[: len(bodies)])
bodies.append(json.loads(request.content))
return httpx.Response(
200,
headers={"content-type": "text/event-stream"},
stream=wires[len(bodies) - 1],
)
p = provider(reply, max_attempts=3)
initial = native_body(True)
try:
result = await delivered(
p.stream_native_messages(NativeMessagesRequest(initial)), "messages"
)
assert_completed(parse_sse_text(result), "messages")
assert public_text(parse_sse_text(result), "messages") == "Hello world again."
assert len(bodies) == 3
assert bodies[-1]["messages"][:-2] == initial["messages"]
assert bodies[-1]["messages"][-2] == {
"role": "assistant",
"content": "Hello world ",
}
assert all(wire.closed for wire in wires)
finally:
await p.cleanup()

View file

@ -0,0 +1,291 @@
"""Recovery exhaustion preserves failure semantics and public stream ownership."""
from copy import deepcopy
from itertools import pairwise
import pytest
from free_claude_code.api.request_outcomes import _observe_event
from free_claude_code.core.anthropic.stream_contracts import parse_sse_text
from free_claude_code.core.continuation_stream import ContinuationStream
from free_claude_code.core.request_outcomes import (
RequestOutcome,
current_request_outcome,
)
from free_claude_code.providers.admission import (
ProviderAdmissionController,
ProviderExecutionState,
)
from free_claude_code.providers.stream_recovery import RecoveryHoldbackBuffer
from tests.api.test_delivered_stream_recovery import (
assert_completed,
public_text,
text_events,
)
from tests.api.test_hidden_stream_retries import delivered
from tests.providers.test_history_transports import _harness
@pytest.fixture
def immediate_events(monkeypatch):
monkeypatch.setattr(
"free_claude_code.providers.stream_recovery.RecoveryHoldbackBuffer",
lambda: RecoveryHoldbackBuffer(max_bytes=1),
)
def assert_unavailable_snapshot(events):
assert events[-1].event == "response.failed"
assert events[-1].data["type"] == "response.failed"
assert sum(event.event == "response.failed" for event in events) == 1
assert not any(event.event == "response.completed" for event in events)
response = events[-1].data["response"]
assert response["status"] == "failed"
assert response["output"] == []
assert response["usage"] is None
assert "snapshot" in response["error"]["message"].lower()
assert response["id"] == events[0].data["response"]["id"]
numbers = [event.data["sequence_number"] for event in events]
assert all(left < right for left, right in pairwise(numbers))
@pytest.mark.asyncio
@pytest.mark.parametrize("protocol", ["chat", "messages", "responses"])
@pytest.mark.parametrize("wire", ["messages", "responses"])
@pytest.mark.parametrize("continuing", [False, True])
async def test_retention_failure_does_not_claim_a_partial_final_snapshot(
monkeypatch, immediate_events, protocol, wire, continuing
):
monkeypatch.setattr(
"free_claude_code.core.delivered_response.RECOVERY_RETENTION_BYTES", 1200
)
def reply(bodies):
text = "First " if continuing and len(bodies) == 1 else "x" * 2000
return 200, text_events(protocol, text, complete=False)
async with _harness(protocol, reply, max_attempts=3) as (send, bodies, _):
raw = await delivered(send(wire, [{"role": "user", "content": "write"}]), wire)
events = parse_sse_text(raw)
assert len(bodies) == (2 if continuing else 1)
assert public_text(events, wire) == ("First " if continuing else "") + "x" * 2000
if wire == "responses":
assert_unavailable_snapshot(events)
else:
assert events[-1].event == "error"
assert sum(event.event == "error" for event in events) == 1
@pytest.mark.asyncio
@pytest.mark.parametrize("limited", [False, True])
@pytest.mark.parametrize("code", [None, "context_length_exceeded"])
async def test_failed_frame_keeps_error_category_and_finishes_execution(
monkeypatch, immediate_events, limited, code
):
if limited:
monkeypatch.setattr(
"free_claude_code.core.delivered_response.RECOVERY_RETENTION_BYTES", 1200
)
executions = []
original = ProviderAdmissionController.start_execution
def capture(self, **kwargs):
execution = original(self, **kwargs)
executions.append(execution)
return execution
monkeypatch.setattr(ProviderAdmissionController, "start_execution", capture)
error = {
"type": "rate_limit_error" if code is None else "invalid_request_error",
"code": code,
"param": "input",
"message": "Provider rejected the request.",
}
def reply(bodies):
if len(bodies) == 1:
return 200, text_events("responses", "First ", complete=False)
events = text_events("responses", "x" * 2000)
events[-1]["type"] = "response.failed"
events[-1]["response"].update(status="failed", error=error)
return 200, events
async with _harness("responses", reply, max_attempts=2) as (send, bodies, _):
raw = await delivered(
send("responses", [{"role": "user", "content": "write"}]), "responses"
)
events = parse_sse_text(raw)
assert len(bodies) == 2
assert executions[-1].state is ProviderExecutionState.FAILED
failure = events[-1].data["response"]
assert failure["error"]["type"] == error["type"]
assert failure["error"]["code"] == code
assert failure["error"]["param"] == "input"
assert failure["error"]["message"].startswith(error["message"])
outcome = RequestOutcome()
token = current_request_outcome.set(outcome)
try:
_observe_event(events[-1])
finally:
current_request_outcome.reset(token)
assert outcome.failure_reason == (code or error["type"])
if limited:
assert_unavailable_snapshot(events)
else:
assert (
"".join(
part.get("text", "")
for item in failure["output"]
for part in item.get("content", [])
)
== "First " + "x" * 2000
)
assert failure["error"] == error
@pytest.mark.asyncio
@pytest.mark.parametrize("candidate", ["", "New text."])
async def test_production_cap_during_closures_does_not_count_as_new_output(candidate):
# The production holdback releases this prefix by size before the disconnect.
prefix = "A" * 400_000
def reply(bodies):
return 200, text_events(
"responses",
prefix if len(bodies) == 1 else candidate,
complete=len(bodies) > 1,
)
async with _harness("responses", reply, max_attempts=2) as (send, bodies, _):
raw = await delivered(
send("responses", [{"role": "user", "content": "write"}]), "responses"
)
events = parse_sse_text(raw)
assert len(bodies) == 2
assert public_text(events, "responses") == prefix + candidate
if candidate:
assert_completed(events, "responses")
else:
assert_unavailable_snapshot(events)
@pytest.mark.asyncio
async def test_cap_during_closures_does_not_turn_pure_echo_into_success(
monkeypatch, immediate_events
):
monkeypatch.setattr(
"free_claude_code.core.delivered_response.RECOVERY_RETENTION_BYTES", 1200
)
async with _harness(
"responses",
lambda bodies: (
200,
text_events("responses", "A" * 600, complete=len(bodies) > 1),
),
max_attempts=2,
) as (send, bodies, _):
raw = await delivered(
send("responses", [{"role": "user", "content": "write"}]), "responses"
)
events = parse_sse_text(raw)
assert len(bodies) == 2
assert public_text(events, "responses") == "A" * 600
assert_unavailable_snapshot(events)
@pytest.mark.asyncio
async def test_cap_failure_formatting_does_not_reenter_a_failed_mapper(
monkeypatch, immediate_events
):
monkeypatch.setattr(
"free_claude_code.core.delivered_response.RECOVERY_RETENTION_BYTES", 1200
)
original = ContinuationStream.prepare
def prepare(self, payload):
if payload["type"] == "response.output_text.done":
raise ValueError("public processing failed")
return original(self, payload)
monkeypatch.setattr(ContinuationStream, "prepare", prepare)
async with _harness(
"responses",
lambda bodies: (
200,
text_events(
"responses",
"First " if len(bodies) == 1 else "x" * 2000,
complete=len(bodies) > 1,
),
),
max_attempts=3,
) as (send, bodies, _):
raw = await delivered(
send("responses", [{"role": "user", "content": "write"}]), "responses"
)
events = parse_sse_text(raw)
assert len(bodies) == 2
assert public_text(events, "responses") == "First " + "x" * 2000
assert_unavailable_snapshot(events)
assert "public processing failed" in events[-1].data["response"]["error"]["message"]
ANNOTATION = {
"type": "url_citation",
"url": "https://example.com/source",
"title": "Source",
"start_index": 0,
"end_index": 5,
}
@pytest.mark.asyncio
@pytest.mark.parametrize("part_event", ["added", "done"])
@pytest.mark.parametrize("wire", ["messages", "responses"])
@pytest.mark.parametrize("continuing", [False, True])
async def test_annotated_part_blocks_continuation_before_its_item_snapshot(
immediate_events, part_event, wire, continuing
):
annotated_attempt = 2 if continuing else 1
def reply(bodies):
if continuing and len(bodies) == 1:
return 200, text_events("responses", "First ", complete=False)
if len(bodies) != annotated_attempt:
return 200, text_events("responses", "Tail.")
events = deepcopy(text_events("responses", "Cited "))[:6]
events[2 if part_event == "added" else 5]["part"]["annotations"] = [ANNOTATION]
return 200, events
async with _harness("responses", reply, max_attempts=3) as (send, bodies, _):
raw = await delivered(send(wire, [{"role": "user", "content": "cite"}]), wire)
events = parse_sse_text(raw)
assert len(bodies) == annotated_attempt
assert events[-1].event == ("error" if wire == "messages" else "response.failed")
assert "Tail." not in public_text(events, wire)
if continuing:
assert not any(
event.data.get("part", {}).get("annotations") for event in events
)
@pytest.mark.asyncio
@pytest.mark.parametrize("annotated", [False, True])
async def test_healthy_original_response_preserves_annotations(
immediate_events, annotated
):
source = text_events("responses", "Cited ")
if annotated:
source[2]["part"]["annotations"] = [ANNOTATION]
source[5]["part"]["annotations"] = [ANNOTATION]
async with _harness("responses", lambda _: (200, source)) as (send, bodies, _):
raw = await delivered(
send("responses", [{"role": "user", "content": "cite"}]), "responses"
)
events = parse_sse_text(raw)
assert len(bodies) == 1
assert_completed(events, "responses")
assert public_text(events, "responses") == "Cited "
assert events[-1].data["response"]["output"][0]["content"][0]["annotations"] == (
[ANNOTATION] if annotated else []
)

View file

@ -0,0 +1,304 @@
"""Recovery preserves content and closes each published lifecycle stage once."""
import asyncio
from collections import Counter
from copy import deepcopy
from typing import Any
import httpx2
import pytest
from free_claude_code.core.anthropic.stream_contracts import parse_sse_text
from free_claude_code.core.delivered_response import DeliveredResponse
from free_claude_code.providers.stream_recovery import RecoveryHoldbackBuffer
from tests.api.test_delivered_stream_recovery import (
assert_completed,
public_text,
text_events,
)
from tests.api.test_hidden_stream_retries import delivered, partial_tool
from tests.api.test_tool_call_buffer_transports import _tools
from tests.providers.test_history_transports import _harness
from tests.providers.test_native_tool_arguments import tool_events
@pytest.fixture(autouse=True)
def release_events(monkeypatch):
monkeypatch.setattr(
"free_claude_code.providers.stream_recovery.RecoveryHoldbackBuffer",
lambda: RecoveryHoldbackBuffer(max_bytes=1),
)
@pytest.mark.asyncio
async def test_partial_closure_with_default_holdback(monkeypatch):
monkeypatch.setattr(
"free_claude_code.providers.stream_recovery.RecoveryHoldbackBuffer",
RecoveryHoldbackBuffer,
)
original_transport = httpx2.MockTransport
class PacedStream(httpx2.AsyncByteStream):
def __init__(self, raw):
self.raw = raw
async def __aiter__(self):
first, _, rest = self.raw.partition(b"\n\n")
yield first + b"\n\n"
await asyncio.sleep(0.8)
yield rest
def transport(handler):
async def handle(request):
response = handler(request)
raw = await response.aread()
await response.aclose()
return httpx2.Response(
200, headers=response.headers, stream=PacedStream(raw)
)
return original_transport(handle)
monkeypatch.setattr(httpx2, "MockTransport", transport)
async with _harness(
"responses",
lambda bodies: (
200,
text_events("responses", "Hello ")[:6]
if len(bodies) == 1
else text_events("responses", "world."),
),
max_attempts=2,
) as (send, bodies, _):
raw = await delivered(
send("responses", [{"role": "user", "content": "greet"}]), "responses"
)
events = parse_sse_text(raw)
assert_completed(events, "responses")
assert len(bodies) == 2
assert "Hello " in str(bodies[1]["input"])
assert public_text(events, "responses") == "Hello world."
assert (
sum(
event.event == "response.content_part.done"
and event.data["item_id"] == "msg_text"
for event in events
)
== 1
)
@pytest.mark.asyncio
@pytest.mark.parametrize("wire", ["messages", "responses"])
@pytest.mark.parametrize(
"candidate,expected",
[
("Hello world.", "Hello world."),
("lo world.", "Hello world."),
("Hel", "Hello Hel"),
],
)
async def test_reasoning_before_continuation_text_keeps_overlap_filter(
wire, candidate, expected
):
def reply(bodies):
if len(bodies) == 1:
return 200, text_events("chat", "Hello ", complete=False)
reasoning = text_events("chat", "", complete=False)[0]
reasoning["choices"][0]["delta"] = {
"reasoning_content": "Continue the greeting."
}
return 200, [reasoning, *text_events("chat", candidate)]
async with _harness("chat", reply, max_attempts=2) as (send, _, _):
raw = await delivered(send(wire, [{"role": "user", "content": "greet"}]), wire)
events = parse_sse_text(raw)
assert_completed(events, wire)
assert public_text(events, wire) == expected
assert "Continue the greeting." in raw
def refusal_events(*, mixed=False):
events = text_events("responses", "lo world." if mixed else "")
part = {"type": "refusal", "refusal": "I cannot continue that request."}
position = 1 if mixed else 0
prefix = events[:-2] if mixed else events[:2]
refusal: list[dict[str, Any]] = [
{
"type": "response.content_part.added",
"part": {"type": "refusal", "refusal": ""},
},
{"type": "response.refusal.delta", "delta": part["refusal"]},
{"type": "response.refusal.done", "refusal": part["refusal"]},
{"type": "response.content_part.done", "part": part},
]
for event in refusal:
event.update(output_index=0, content_index=position, item_id="msg_text")
item = events[-2]["item"]
item["content"] = [*item["content"], part] if mixed else [part]
item["phase"] = "final_answer"
return [
{**event, "sequence_number": index}
for index, event in enumerate([*prefix, *refusal, *events[-2:]])
]
@pytest.mark.asyncio
@pytest.mark.parametrize("mixed", [False, True])
@pytest.mark.parametrize("limited", [False, True])
async def test_continuation_preserves_refusal_and_completed_metadata(
monkeypatch, mixed, limited
):
if limited:
monkeypatch.setattr(
"free_claude_code.core.delivered_response.RECOVERY_RETENTION_BYTES", 600
)
async with _harness(
"responses",
lambda bodies: (
200,
text_events("responses", "Hello ", complete=False)
if len(bodies) == 1
else refusal_events(mixed=mixed),
),
max_attempts=2,
) as (send, bodies, _):
raw = await delivered(
send("responses", [{"role": "user", "content": "greet"}]), "responses"
)
events = parse_sse_text(raw)
assert_completed(events, "responses")
assert len(bodies) == 2
output = events[-1].data["response"]["output"]
assert output == [
event.data["item"]
for event in events
if event.event == "response.output_item.done"
]
assert output[-1]["phase"] == "final_answer"
assert output[-1]["content"][-1] == {
"type": "refusal",
"refusal": "I cannot continue that request.",
}
assert public_text(events, "responses") == ("Hello world." if mixed else "Hello ")
@pytest.mark.asyncio
async def test_published_refusal_interruption_does_not_generate_again():
async with _harness(
"responses",
lambda bodies: (
200,
text_events("responses", "Hello ", complete=False)
if len(bodies) == 1
else refusal_events()[:-3],
),
max_attempts=3,
) as (send, bodies, _):
raw = await delivered(
send("responses", [{"role": "user", "content": "greet"}]), "responses"
)
events = parse_sse_text(raw)
assert len(bodies) == 2
assert events[-1].event == "response.failed"
assert (
events[-1].data["response"]["output"][-1]["content"][-1]["refusal"]
== "I cannot continue that request."
)
@pytest.mark.asyncio
@pytest.mark.parametrize("cut", [4, 5, 6, 7])
async def test_recovery_emits_only_missing_text_completion_stages(cut):
async with _harness(
"responses",
lambda bodies: (
200,
text_events("responses", "Hello ")[:cut]
if len(bodies) == 1
else text_events("responses", "world."),
),
max_attempts=2,
) as (send, _, _):
raw = await delivered(
send("responses", [{"role": "user", "content": "greet"}]), "responses"
)
events = parse_sse_text(raw)
assert_completed(events, "responses")
counts = Counter(
event.event
for event in events
if event.data.get("item_id", event.data.get("item", {}).get("id")) == "msg_text"
)
for kind in (
"response.output_text.done",
"response.content_part.done",
"response.output_item.done",
):
assert counts[kind] == 1
assert public_text(events, "responses") == "Hello world."
@pytest.mark.asyncio
@pytest.mark.parametrize("limited", [False, True])
@pytest.mark.parametrize("continued", [False, True])
@pytest.mark.parametrize("cut", [4, 5, 6])
async def test_handoff_snapshot_matches_published_closures(
monkeypatch, limited, continued, cut
):
if limited:
original = DeliveredResponse.closing_events
def cap_during_boundary(self):
events = original(self)
if self.snapshot().has_calls:
monkeypatch.setattr(
"free_claude_code.core.delivered_response.RECOVERY_RETENTION_BYTES",
self._bytes + 1,
)
return events
monkeypatch.setattr(DeliveredResponse, "closing_events", cap_during_boundary)
def reply(bodies):
if continued and len(bodies) == 1:
return 200, text_events("responses", "Before. ", complete=False)
events = deepcopy(tool_events("responses", '{"path":"kept"}')[:-1])
for event in text_events("responses", "Next ")[1:cut]:
event["output_index"] = 1
events.append(event)
for event in partial_tool("responses")[1:]:
if "output_index" in event:
event["output_index"] = 2
events.append(event)
return 200, [
{**event, "sequence_number": index} for index, event in enumerate(events)
]
async with _harness("responses", reply, max_attempts=2 if continued else 1) as (
send,
bodies,
_,
):
raw = await delivered(
send(
"responses",
[{"role": "user", "content": "read"}],
tools=_tools("responses"),
),
"responses",
)
events = parse_sse_text(raw)
assert_completed(events, "responses")
assert len(bodies) == (2 if continued else 1)
assert "call_abandoned" not in raw
output = events[-1].data["response"]["output"]
assert output == [
event.data["item"]
for event in events
if event.event == "response.output_item.done"
]
assert all(item["status"] == "completed" for item in output)
assert [
item["arguments"] for item in output if item["type"] == "function_call"
] == ['{"path":"kept"}']

View file

@ -0,0 +1,364 @@
"""Attempt completion and public publication share one response lifecycle."""
from copy import deepcopy
import pytest
from free_claude_code.core.anthropic.stream_contracts import parse_sse_text
from free_claude_code.core.continuation_stream import ContinuationStream
from free_claude_code.core.stream_delivery import current_stream_delivery
from free_claude_code.providers.request_recovery import RequestCorrections
from free_claude_code.providers.stream_recovery import RecoveryHoldbackBuffer
from tests.api.test_delivered_stream_recovery import public_text, text_events
from tests.api.test_hidden_stream_retries import delivered
from tests.api.test_tool_call_buffer import _call, _frames, _response, _start
from tests.api.test_tool_call_buffer_transports import _tools
from tests.providers.test_history_transports import _harness
from tests.providers.test_native_tool_arguments import tool_events
@pytest.fixture(autouse=True)
def immediate_events(monkeypatch):
monkeypatch.setattr(
"free_claude_code.providers.stream_recovery.RecoveryHoldbackBuffer",
lambda: RecoveryHoldbackBuffer(max_bytes=1),
)
@pytest.mark.asyncio
@pytest.mark.parametrize("preceding_text", [False, True])
@pytest.mark.parametrize("snapshot_only", [False, True])
async def test_continuation_preserves_call_completed_by_terminal_snapshot(
preceding_text, snapshot_only
):
def reply(bodies):
if len(bodies) == 1:
return 200, text_events("responses", "Hello ", complete=False)
tool = deepcopy(tool_events("responses", '{"path":"file"}'))
if preceding_text:
text = text_events("responses", "world.")
for event in tool:
if "output_index" in event:
event["output_index"] = 1
tool[-1]["response"]["output"].insert(0, text[-2]["item"])
events = [
*(text[:-1] if preceding_text else tool[:1]),
*(
[e for e in tool[1:-1] if e["type"] != "response.output_item.done"]
if not snapshot_only
else []
),
tool[-1],
]
return 200, [{**event, "sequence_number": i} for i, event in enumerate(events)]
async with _harness("responses", reply, max_attempts=2) as (send, bodies, _):
raw = await delivered(
send(
"responses",
[{"role": "user", "content": "greet and read"}],
tools=_tools("responses"),
),
"responses",
)
events = parse_sse_text(raw)
assert len(bodies) == 2
assert events[-1].event == "response.completed"
output = events[-1].data["response"]["output"]
calls = [item for item in output if item["type"] == "function_call"]
assert len(calls) == 1
assert calls[0]["call_id"] == "call_probe"
assert calls[0]["arguments"] == '{"path":"file"}'
assert calls[0]["status"] == "completed"
assert public_text(events, "responses") == "Hello " + (
"world." if preceding_text else ""
)
@pytest.mark.asyncio
@pytest.mark.parametrize("activity", ["metadata", "withheld_tools", "text"])
async def test_body_correction_depends_on_current_attempt_publication(activity):
visible = activity == "text"
def reply(bodies):
if len(bodies) == 1:
return 200, text_events("responses", "Hello ", complete=False)
if len(bodies) == 2:
prefix = text_events("responses", "world ", complete=False)
hidden = _frames(
"responses", [_call("responses", 0)[0], _call("responses", 1)[0]]
)
return 200, [
*(prefix if visible else prefix[:1]),
*(
[event.data for frame in hidden for event in parse_sse_text(frame)]
if activity == "withheld_tools"
else []
),
{
"type": "error",
"sequence_number": 4,
"code": "invalid_encrypted_content",
"param": "input[0].encrypted_content",
"message": "The encrypted content could not be verified.",
},
]
return 200, text_events("responses", "world again.")
history = [
{
"type": "reasoning",
"id": "rs_old",
"encrypted_content": "opaque",
"summary": [],
},
{"role": "user", "content": "greet"},
]
async with _harness("responses", reply, max_attempts=3) as (send, bodies, _):
raw = await delivered(send("responses", history), "responses")
events = parse_sse_text(raw)
assert len(bodies) == (2 if visible else 3)
assert events[-1].event == ("response.failed" if visible else "response.completed")
assert public_text(events, "responses") == (
"Hello world " if visible else "Hello world again."
)
assert "call_0" not in raw and "call_1" not in raw
@pytest.mark.asyncio
async def test_released_tool_group_is_published_one_frame_at_a_time():
resumed = []
states = []
async def source():
state = current_stream_delivery()
assert state is not None
states.append(state)
yield "".join(_frames("responses", [_start("responses")]))
yield "".join(_frames("responses", _call("responses")))
resumed.append(state.content_released)
response = await _response("responses", source())
try:
iterator = aiter(response.body_iterator)
assert parse_sse_text(str(await anext(iterator)))[0].event == "response.created"
first = parse_sse_text(str(await anext(iterator)))
assert len(first) == 1
assert first[0].event == "response.output_item.added"
assert not states[0].prefix.has_calls
assert resumed == []
assert len(parse_sse_text(str(await anext(iterator)))) == 1
assert resumed == []
await anext(iterator)
await anext(iterator)
assert resumed == []
assert [part async for part in iterator] == []
assert resumed == [True]
finally:
await response.aclose()
@pytest.mark.asyncio
async def test_public_transform_failure_preserves_returned_frames_and_emits_error(
monkeypatch,
):
original = ContinuationStream.prepare
closed = []
def prepare(self, payload):
if payload["type"] == "response.output_text.done":
raise ValueError("public transform failed")
return original(self, payload)
monkeypatch.setattr(ContinuationStream, "prepare", prepare)
async def source():
state = current_stream_delivery()
assert state is not None
try:
for event in text_events("responses", "Hello ", complete=False):
yield _frames("responses", [(event["type"], event)])[0]
state.begin_continuation()
yield "".join(
_frames(
"responses",
[
(event["type"], event)
for event in text_events("responses", "world.")
],
)
)
finally:
closed.append(True)
raw = await delivered(source(), "responses")
events = parse_sse_text(raw)
assert public_text(events, "responses") == "Hello world."
assert events[-1].event == "response.failed"
assert sum(e.event == "response.failed" for e in events) == 1
assert not any(e.event == "response.completed" for e in events)
assert closed == [True]
@pytest.mark.asyncio
@pytest.mark.parametrize("text", ["Hello world.", "world.", "Hello ", ""])
async def test_terminal_only_text_uses_the_same_overlap_and_progress_rule(text):
def reply(bodies):
if len(bodies) == 1:
return 200, text_events("responses", "Hello ", complete=False)
events = text_events("responses", text)
return 200, [events[0], events[-1]]
async with _harness("responses", reply, max_attempts=3) as (send, bodies, _):
raw = await delivered(
send("responses", [{"role": "user", "content": "greet"}]), "responses"
)
events = parse_sse_text(raw)
assert len(bodies) == 2
assert events[-1].event == (
"response.completed" if "world" in text else "response.failed"
)
if "world" in text:
output = events[-1].data["response"]["output"]
assert (
"".join(part["text"] for item in output for part in item.get("content", []))
== "Hello world."
)
@pytest.mark.asyncio
@pytest.mark.parametrize("kind", ["reasoning", "web_search_call", "message"])
async def test_final_only_native_state_is_checked_before_continuation_publication(kind):
def reply(bodies):
if len(bodies) == 1:
return 200, text_events("responses", "Hello ", complete=False)
events = text_events("responses", "")
item = {"id": "native_unreplayable", "type": kind, "status": "completed"}
if kind == "reasoning":
item.update(encrypted_content="opaque", summary=[])
elif kind == "message":
item.update(role="assistant", content=[{"type": "future_native_part"}])
events[-1]["response"]["output"] = [item]
return 200, [events[0], events[-1]]
async with _harness("responses", reply, max_attempts=3) as (send, bodies, _):
raw = await delivered(
send("responses", [{"role": "user", "content": "greet"}]), "responses"
)
assert len(bodies) == 2
assert parse_sse_text(raw)[-1].event == "response.failed"
assert "native_unreplayable" not in raw
@pytest.mark.asyncio
async def test_filtered_snapshot_does_not_change_overlap_target_identity():
def reply(bodies):
if len(bodies) == 1:
return 200, text_events("responses", "Hello ", complete=False)
text = text_events("responses", "Hello world.")
for event in text:
if "output_index" in event:
event["output_index"] = 1
call = _call("responses", 0)[0]
terminal = text[-1]
terminal["response"]["output"].insert(0, call[1]["item"])
events = [*text[:-1], {"type": call[0], **call[1]}, terminal]
return 200, [{**event, "sequence_number": i} for i, event in enumerate(events)]
async with _harness("responses", reply, max_attempts=2) as (send, _, _):
raw = await delivered(
send("responses", [{"role": "user", "content": "greet"}]), "responses"
)
events = parse_sse_text(raw)
assert "call_0" not in raw
assert public_text(events, "responses") == "Hello world."
assert [
item["content"][0]["text"] for item in events[-1].data["response"]["output"]
] == ["Hello ", "world."]
@pytest.mark.asyncio
async def test_attempt_resets_are_atomic_and_old_closures_do_not_commit_new_attempt():
attempts = []
async def source():
state = current_stream_delivery()
assert state is not None
state.begin_attempt()
for event in text_events("responses", "Hello ", complete=False):
yield _frames("responses", [(event["type"], event)])[0]
state.begin_continuation()
state.begin_attempt()
yield _frames("responses", [_start("responses")])[0]
attempts.append(state.attempt_content_released)
yield "".join(_frames("responses", _call("responses")[:2]))
yield 'event: response.output_item.added\ndata: {"type":'
attempts.append(state.attempt_content_released)
# Several invisible failures can occur in one read of the provider iterator.
state.begin_attempt()
state.begin_attempt()
for event in text_events("responses", "world."):
yield _frames("responses", [(event["type"], event)])[0]
attempts.append(state.attempt_content_released)
raw = await delivered(source(), "responses")
events = parse_sse_text(raw)
assert attempts == [False, False, True]
assert public_text(events, "responses") == "Hello world."
assert "call_0" not in raw
assert events[-1].event == "response.completed"
assert sum(e.event == "response.created" for e in events) == 1
@pytest.mark.asyncio
@pytest.mark.parametrize("protocol", ["chat", "messages", "responses"])
@pytest.mark.parametrize("wire", ["messages", "responses"])
@pytest.mark.parametrize("visible", [False, True])
async def test_shared_correction_guard_uses_public_attempt_effects(
monkeypatch, protocol, wire, visible
):
marker = "injected request correction"
proposals = []
def correction(self, error, body, **kwargs):
if marker not in str(error):
return None
proposals.append(True)
return {**deepcopy(body), "temperature": 0}
monkeypatch.setattr(RequestCorrections, "next_body", correction)
def reply(bodies):
if len(bodies) == 1:
return 200, text_events(protocol, "Hello ", complete=False)
if len(bodies) == 2:
events = text_events(protocol, "world " if visible else "", complete=False)
if not visible and protocol != "chat":
events = events[:1]
error = {
"type": "invalid_request_error",
"message": marker,
"code": "invalid_request",
}
events.append(
{
"type": "error",
**{key: value for key, value in error.items() if key != "type"},
}
if protocol == "responses"
else {"type": "error", "error": error}
)
return 200, events
return 200, text_events(protocol, "world.")
async with _harness(protocol, reply, max_attempts=3) as (send, bodies, _):
raw = await delivered(send(wire, [{"role": "user", "content": "greet"}]), wire)
events = parse_sse_text(raw)
assert len(bodies) == (2 if visible else 3)
assert proposals == ([] if visible else [True])
assert public_text(events, wire) == ("Hello world " if visible else "Hello world.")
assert events[-1].event == (
("error" if wire == "messages" else "response.failed")
if visible
else ("message_stop" if wire == "messages" else "response.completed")
)

View file

@ -0,0 +1,344 @@
"""Recovery observes native reasoning without changing its wire or replay meaning."""
from copy import deepcopy
import pytest
from free_claude_code.core.anthropic import aggregate_anthropic_sse_to_message
from free_claude_code.core.anthropic.stream_contracts import parse_sse_text
from free_claude_code.core.history_replay import decode_replay
from free_claude_code.providers.stream_recovery import RecoveryHoldbackBuffer
from tests.api.test_delivered_stream_recovery import (
assert_completed,
public_text,
text_events,
)
from tests.api.test_hidden_stream_retries import delivered
from tests.providers.test_history_transports import _events_for, _harness
@pytest.fixture(autouse=True)
def immediate_events(monkeypatch):
monkeypatch.setattr(
"free_claude_code.providers.stream_recovery.RecoveryHoldbackBuffer",
lambda: RecoveryHoldbackBuffer(max_bytes=1),
)
def reasoning_events(*, field="summary", seeded="", closed=True):
events = deepcopy(_events_for("responses"))
part_type = "summary_text" if field == "summary" else "reasoning_text"
for event in events:
item = event.get("item")
items = (
[item] if item is not None else event.get("response", {}).get("output", [])
)
for item in items:
item.pop("encrypted_content", None)
item["content"] = None
item["summary"] = []
text = seeded if event["type"].endswith(".added") else seeded + "Find 17."
if text:
item[field] = [{"type": part_type, "text": text}]
if (
event["type"] == "response.reasoning_summary_text.delta"
and field == "content"
):
event["type"] = "response.reasoning_text.delta"
event["content_index"] = event.pop("summary_index")
return events if closed else events[:3]
def followed_by_text(events, text):
result = deepcopy(events)
tail = text_events("responses", text, complete=False)[1:]
for event in tail:
if "output_index" in event:
event["output_index"] = 1
return [
{**event, "sequence_number": index}
for index, event in enumerate([*result, *tail])
]
@pytest.mark.asyncio
@pytest.mark.parametrize("wire", ["messages", "responses"])
async def test_nullable_reasoning_content_preserves_healthy_response(wire):
events = _events_for("responses")
for event in events:
if "item" in event:
event["item"]["content"] = None
for item in event.get("response", {}).get("output", []):
item["content"] = None
async with _harness("responses", lambda _: (200, events), max_attempts=1) as (
send,
bodies,
_,
):
raw = await delivered(send(wire, [{"role": "user", "content": "solve"}]), wire)
assert len(bodies) == 1
assert_completed(parse_sse_text(raw), wire)
if wire == "responses":
item = parse_sse_text(raw)[-1].data["response"]["output"][0]
assert item["content"] is None
assert (
decode_replay(item["encrypted_content"]).native
== events[-1]["response"]["output"][0]
)
@pytest.mark.asyncio
@pytest.mark.parametrize("field", ["summary", "content"])
@pytest.mark.parametrize("closed", [False, True])
@pytest.mark.parametrize("wire", ["messages", "responses"])
async def test_readable_reasoning_continues_from_released_content(field, closed, wire):
first = reasoning_events(field=field, closed=closed)
if closed:
first = first[:-1]
first = followed_by_text(first, "Hello ")
async with _harness(
"responses",
lambda bodies: (
200,
first if len(bodies) == 1 else text_events("responses", "world."),
),
max_attempts=2,
) as (send, bodies, _):
raw = await delivered(send(wire, [{"role": "user", "content": "solve"}]), wire)
events = parse_sse_text(raw)
assert len(bodies) == 2
assert_completed(events, wire)
assert public_text(events, wire) == "Hello world."
assert "Find 17." in str(bodies[1]["input"])
assert "Hello " in str(bodies[1]["input"])
@pytest.mark.asyncio
@pytest.mark.parametrize("wire", ["messages", "responses"])
async def test_readable_reasoning_is_allowed_in_the_continuation(wire):
async with _harness(
"responses",
lambda bodies: (
200,
text_events("responses", "Hello ", complete=False)
if len(bodies) == 1
else reasoning_events(),
),
max_attempts=2,
) as (send, bodies, _):
raw = await delivered(send(wire, [{"role": "user", "content": "solve"}]), wire)
events = parse_sse_text(raw)
assert len(bodies) == 2
assert_completed(events, wire)
assert public_text(events, wire) == "Hello "
@pytest.mark.asyncio
@pytest.mark.parametrize("wire", ["messages", "responses"])
async def test_completed_readable_carrier_without_deltas_counts_as_output(wire):
continuation = reasoning_events()
continuation.pop(2)
async with _harness(
"responses",
lambda bodies: (
200,
text_events("responses", "Hello ", complete=False)
if len(bodies) == 1
else continuation,
),
max_attempts=2,
) as (send, bodies, _):
raw = await delivered(send(wire, [{"role": "user", "content": "solve"}]), wire)
assert len(bodies) == 2
assert_completed(parse_sse_text(raw), wire)
@pytest.mark.asyncio
@pytest.mark.parametrize("wire", ["messages", "responses"])
async def test_completed_readable_carrier_without_deltas_is_continuation_context(wire):
first = reasoning_events()[:-1]
first.pop(2)
first = followed_by_text(first, "Hello ")
async with _harness(
"responses",
lambda bodies: (
200,
first if len(bodies) == 1 else text_events("responses", "world."),
),
max_attempts=2,
) as (send, bodies, _):
raw = await delivered(send(wire, [{"role": "user", "content": "solve"}]), wire)
assert len(bodies) == 2
assert_completed(parse_sse_text(raw), wire)
assert "Find 17." in str(bodies[1]["input"])
@pytest.mark.asyncio
async def test_nullable_reasoning_in_terminal_only_continuation_is_output():
continuation = reasoning_events()
async with _harness(
"responses",
lambda bodies: (
200,
text_events("responses", "Hello ", complete=False)
if len(bodies) == 1
else [continuation[0], continuation[-1]],
),
max_attempts=2,
) as (send, bodies, _):
raw = await delivered(
send("responses", [{"role": "user", "content": "solve"}]), "responses"
)
assert len(bodies) == 2
events = parse_sse_text(raw)
assert_completed(events, "responses")
assert events[-1].data["response"]["output"][-1]["summary"][0]["text"] == "Find 17."
@pytest.mark.asyncio
@pytest.mark.parametrize("field", ["summary", "content"])
@pytest.mark.parametrize("seeded", ["", "First, "])
async def test_synthetic_reasoning_closure_preserves_followup_history(field, seeded):
first = followed_by_text(
reasoning_events(field=field, seeded=seeded, closed=False), "Hello "
)
def reply(bodies):
if len(bodies) == 1:
return 200, first
return 200, text_events("responses", "world." if len(bodies) == 2 else "Next.")
async with _harness("responses", reply, max_attempts=2) as (send, bodies, _):
raw = await delivered(
send("responses", [{"role": "user", "content": "solve"}]), "responses"
)
events = parse_sse_text(raw)
assert_completed(events, "responses")
output = events[-1].data["response"]["output"]
history = [*deepcopy(output), {"role": "user", "content": "Next question."}]
await delivered(send("responses", history), "responses")
assert len(bodies) == 3
native = bodies[2]["input"][0]
assert native["type"] == "reasoning"
assert native["id"] == first[1]["item"]["id"]
assert native["status"] == "completed"
assert native[field][0]["text"] == seeded + "Find 17."
assert native["extension"] == first[1]["item"]["extension"]
assert "previous provider stream" not in str(bodies[2])
assert history[:-1] == output
@pytest.mark.asyncio
@pytest.mark.parametrize("wire", ["messages", "responses"])
async def test_readable_reasoning_survives_repeated_continuation_and_followup(wire):
second = followed_by_text(reasoning_events()[:-1], "middle ")
def reply(bodies):
if len(bodies) == 1:
return 200, text_events("responses", "First ", complete=False)
if len(bodies) == 2:
return 200, second
return 200, text_events("responses", "last." if len(bodies) == 3 else "Next.")
async with _harness("responses", reply, max_attempts=3) as (send, bodies, _):
raw = await delivered(send(wire, [{"role": "user", "content": "solve"}]), wire)
events = parse_sse_text(raw)
assert_completed(events, wire)
assert public_text(events, wire) == "First middle last."
assert "Find 17." in str(bodies[2]["input"])
if wire == "responses":
output = events[-1].data["response"]["output"]
else:
async def chunks():
yield raw
message, error, stopped = await aggregate_anthropic_sse_to_message(chunks())
assert error is None and stopped
output = [{"role": "assistant", "content": message["content"]}]
await delivered(
send(wire, [*output, {"role": "user", "content": "Next question."}]), wire
)
assert len(bodies) == 4
native = next(
item for item in bodies[3]["input"] if item.get("type") == "reasoning"
)
assert native == second[3]["item"]
assert "previous provider stream" not in str(bodies[3])
@pytest.mark.asyncio
@pytest.mark.parametrize("native", ["annotations", "encrypted_reasoning"])
@pytest.mark.parametrize(
"failure", ["failed_context", "incomplete_context", "failed_server"]
)
@pytest.mark.parametrize("wire", ["messages", "responses"])
async def test_native_failure_snapshot_preserves_error_and_blocks_recovery(
native, failure, wire
):
error = {
"code": "server_error"
if failure == "failed_server"
else "context_length_exceeded",
"type": "api_error" if failure == "failed_server" else "invalid_request_error",
"param": "input",
"message": "Model failed to continue.",
}
def reply(bodies):
if len(bodies) == 1:
return 200, text_events("responses", "Hello ", complete=False)
events = text_events("responses", "Unpublished failure snapshot")
final = deepcopy(events[-1])
response = final["response"]
if failure == "incomplete_context":
final["type"] = "response.incomplete"
response.update(
status="incomplete",
incomplete_details={"reason": "model_context_window_exceeded"},
)
else:
final["type"] = "response.failed"
response.update(status="failed", error=error)
if native == "annotations":
response["output"][0]["content"][0]["annotations"] = [
{
"type": "url_citation",
"url": "https://example.com",
"title": "Example",
"start_index": 0,
"end_index": 5,
}
]
else:
response["output"].append(
{
"type": "reasoning",
"id": "rs_final",
"summary": [],
"content": None,
"encrypted_content": "opaque",
"status": "completed",
}
)
return 200, [events[0], final]
async with _harness("responses", reply, max_attempts=3) as (send, bodies, _):
raw = await delivered(send(wire, [{"role": "user", "content": "solve"}]), wire)
events = parse_sse_text(raw)
assert len(bodies) == 2
assert events[-1].event == ("error" if wire == "messages" else "response.failed")
assert public_text(events, wire) == "Hello "
assert "Unpublished failure snapshot" not in raw
result = (
events[-1].data["error"]
if wire == "messages"
else events[-1].data["response"]["error"]
)
assert result["type"] == error["type"]
if failure == "failed_server":
assert error["message"] in result["message"]
if wire == "responses":
assert result["code"] == error["code"]
if failure != "incomplete_context":
assert result == error

View file

@ -0,0 +1,467 @@
"""Recovery must not fabricate structured items or misalign token metadata."""
import json
from copy import deepcopy
import httpx
import httpx2
import pytest
from openai import AsyncOpenAI
from pydantic import BaseModel, ConfigDict
from free_claude_code.core.anthropic.models import MessagesRequest
from free_claude_code.core.anthropic.passthrough import NativeMessagesRequest
from free_claude_code.core.anthropic.stream_contracts import parse_sse_text
from free_claude_code.core.openai_responses import OpenAIResponsesRequest
from free_claude_code.core.reasoning import ReasoningPolicy
from free_claude_code.providers.stream_recovery import RecoveryHoldbackBuffer
from tests.api.test_delivered_stream_recovery import (
assert_completed,
public_text,
text_events,
)
from tests.api.test_hidden_stream_retries import delivered
from tests.api.test_tool_call_buffer_transports import _tools
from tests.providers.test_anthropic_messages_transport import Endpoint, _sse
from tests.providers.test_anthropic_provider import native_body, provider
from tests.providers.test_history_transports import _events_for, _harness
from tests.providers.test_native_tool_arguments import tool_events
@pytest.fixture(autouse=True)
def release_provider_events(monkeypatch):
monkeypatch.setattr(
"free_claude_code.providers.stream_recovery.RecoveryHoldbackBuffer",
lambda: RecoveryHoldbackBuffer(max_bytes=1),
)
class Answer(BaseModel):
model_config = ConfigDict(extra="forbid")
value: int
def output_format(kind):
if kind != "json_schema":
return {"type": kind}
return {
"type": kind,
"name": "Answer",
"schema": Answer.model_json_schema(),
"strict": True,
}
def responses_stream(transport, protocol, **fields):
options = {
"response_model": "public-alias",
"request_id": "text-contract",
"reasoning": ReasoningPolicy.provider_default(),
}
if protocol == "messages":
options["endpoint_context"] = Endpoint()
else:
options["input_tokens"] = 0
return transport.stream_responses(
OpenAIResponsesRequest(model="requested", input="Return value 1", **fields),
**options,
)
async def consume_structured(raw, *, failed):
http = httpx2.AsyncClient(
transport=httpx2.MockTransport(
lambda _: httpx2.Response(
200, headers={"content-type": "text/event-stream"}, content=raw.encode()
)
)
)
async with (
AsyncOpenAI(
api_key="fixture",
base_url="https://consumer.invalid",
max_retries=0,
http_client=http,
) as client,
client.responses.stream(
model="fixture", input="Return value 1", text_format=Answer
) as stream,
):
if failed:
with pytest.raises(RuntimeError, match=r"response\.completed"):
await stream.get_final_response()
else:
response = await stream.get_final_response()
assert response.output_parsed == Answer(value=1)
@pytest.mark.asyncio
@pytest.mark.parametrize(
"protocol,kind",
[
("responses", "json_schema"),
("responses", "json_object"),
("chat", "json_schema"),
("chat", "json_object"),
("messages", "json_schema"),
],
)
@pytest.mark.parametrize("stage", ["healthy", "hidden_retry", "delivered_failure"])
async def test_structured_request_keeps_healthy_output_and_failure_policy(
protocol, kind, stage
):
def reply(bodies):
if len(bodies) == 1 and stage == "hidden_retry":
return 200, []
interrupted = len(bodies) == 1 and stage == "delivered_failure"
# No echoed format: the request itself must protect this contract.
return 200, text_events(
protocol,
'{"value":' if interrupted else '{"value":1}',
complete=not interrupted,
)
async with _harness(protocol, reply, max_attempts=3) as (_, bodies, transport):
raw = await delivered(
responses_stream(transport, protocol, text={"format": output_format(kind)}),
"responses",
)
events = parse_sse_text(raw)
failed = stage == "delivered_failure"
assert len(bodies) == (2 if stage == "hidden_retry" else 1)
assert public_text(events, "responses") == (
'{"value":' if failed else '{"value":1}'
)
if failed:
assert events[-1].event == "response.failed"
assert not any(event.event == "response.completed" for event in events)
if protocol != "chat":
assert not any(
event.event == "response.output_text.done" for event in events
)
else:
assert_completed(events, "responses")
# Chat's existing failure writer closes partial text before reporting failure.
# Its typed-JSON parsing limitation predates delivered-output recovery.
if not failed or protocol != "chat":
await consume_structured(raw, failed=failed)
@pytest.mark.asyncio
@pytest.mark.parametrize("protocol", ["chat", "responses", "messages"])
async def test_explicit_plain_text_format_can_continue(protocol):
async with _harness(
protocol,
lambda bodies: (
200,
text_events(
protocol,
"Hello " if len(bodies) == 1 else "world.",
complete=len(bodies) > 1,
),
),
) as (_, bodies, transport):
raw = await delivered(
responses_stream(transport, protocol, text={"format": {"type": "text"}}),
"responses",
)
assert len(bodies) == 2
assert_completed(parse_sse_text(raw), "responses")
assert public_text(parse_sse_text(raw), "responses") == "Hello world."
@pytest.mark.asyncio
@pytest.mark.parametrize(
"path", ["native_messages", "translated_messages", "chat_extra"]
)
async def test_messages_structured_request_does_not_resume_partial_json(path):
bodies = []
body = {
"model": "requested",
"messages": [{"role": "user", "content": "Return value 1"}],
"max_tokens": 64,
}
schema = {"type": "json_schema", "schema": Answer.model_json_schema()}
if path == "chat_extra":
body["extra_body"] = {
"response_format": {
"type": "json_schema",
"json_schema": {
key: value
for key, value in output_format("json_schema").items()
if key != "type"
},
}
}
else:
body["output_config"] = {"format": schema}
if path == "native_messages":
def reply(request):
bodies.append(json.loads(request.content))
return httpx.Response(
200,
headers={"content-type": "text/event-stream"},
content=_sse(*text_events("messages", '{"value":', complete=False)),
)
p = provider(reply)
try:
raw = await delivered(
p.stream_native_messages(
NativeMessagesRequest({**native_body(True), **body})
),
"messages",
)
finally:
await p.cleanup()
else:
protocol = "chat" if path == "chat_extra" else "messages"
async with _harness(
protocol,
lambda _: (200, text_events(protocol, '{"value":', complete=False)),
max_attempts=2,
) as (_, bodies, transport):
options = {
"response_model": "public-alias",
"request_id": "text-contract",
"reasoning": ReasoningPolicy.provider_default(),
}
if protocol == "messages":
options["endpoint_context"] = Endpoint()
else:
options["input_tokens"] = 0
raw = await delivered(
transport.stream_messages(
MessagesRequest.model_validate(body), **options
),
"messages",
)
assert len(bodies) == 1
events = parse_sse_text(raw)
assert public_text(events, "messages") == '{"value":'
assert events[-1].event == "error"
def scored_events(text, location, *, value=True):
events = [deepcopy(event) for event in text_events("responses", text)]
scores = (
[
{
"token": text,
"bytes": list(text.encode()),
"logprob": -0.5,
"top_logprobs": [],
}
]
if value is True
else value
)
targets = {
"delta": events[3],
"text_done": events[4],
"part": events[5]["part"],
"item": events[6]["item"]["content"][0],
"terminal": events[7]["response"]["output"][0]["content"][0],
}
for key, target in targets.items():
if location == "all" or location == key:
target["logprobs"] = deepcopy(scores)
return events
@pytest.mark.asyncio
@pytest.mark.parametrize(
"continuing,location",
[(False, location) for location in ("delta", "text_done", "part", "item")]
+ [
(True, location)
for location in ("delta", "text_done", "part", "item", "terminal")
],
)
async def test_preserved_logprobs_prevent_text_reconstruction(location, continuing):
def reply(bodies):
if continuing and len(bodies) == 1:
return 200, text_events("responses", "Hello ", complete=False)
events = scored_events("Hello world." if continuing else "Hello ", location)
if not continuing:
events = events[
: {"delta": 4, "text_done": 5, "part": 6, "item": 7}[location]
]
return 200, events
async with _harness("responses", reply, max_attempts=3) as (send, bodies, _):
raw = await delivered(
send("responses", [{"role": "user", "content": "hello"}]), "responses"
)
events = parse_sse_text(raw)
assert len(bodies) == (2 if continuing else 1)
assert events[-1].event == "response.failed"
assert public_text(events, "responses").startswith("Hello ")
if not continuing:
assert public_text(events, "responses") == "Hello "
else:
rejected_kind = {
"delta": "response.output_text.delta",
"text_done": "response.output_text.done",
"part": "response.content_part.done",
"item": "response.output_item.done",
"terminal": "response.completed",
}[location]
assert not any(
event.event == rejected_kind and event.data.get("output_index", 1) == 1
for event in events
)
@pytest.mark.asyncio
@pytest.mark.parametrize("value", [None, []])
async def test_empty_logprobs_do_not_block_recovery(value):
async with _harness(
"responses",
lambda bodies: (
200,
text_events("responses", "Hello ", complete=False)
if len(bodies) == 1
else scored_events("Hello world.", "all", value=value),
),
) as (send, bodies, _):
raw = await delivered(
send("responses", [{"role": "user", "content": "hello"}]), "responses"
)
assert len(bodies) == 2
assert_completed(parse_sse_text(raw), "responses")
assert public_text(parse_sse_text(raw), "responses") == "Hello world."
@pytest.mark.asyncio
async def test_healthy_logprob_payloads_are_preserved():
source = scored_events("Hello world.", "all")
async with _harness("responses", lambda _: (200, source)) as (send, bodies, _):
raw = await delivered(
send("responses", [{"role": "user", "content": "hello"}]), "responses"
)
events = parse_sse_text(raw)
assert len(bodies) == 1
assert_completed(events, "responses")
by_kind = {event.event: event.data for event in events}
assert by_kind["response.output_text.delta"]["logprobs"] == source[3]["logprobs"]
assert events[-1].data["response"]["output"] == source[-1]["response"]["output"]
@pytest.mark.asyncio
@pytest.mark.parametrize("continuing", [False, True])
async def test_echoed_structured_format_prevents_reconstruction(continuing):
def reply(bodies):
events = text_events(
"responses",
"Hello " if len(bodies) == 1 else "world.",
complete=len(bodies) > 1,
)
if not continuing or len(bodies) > 1:
events[0]["response"]["text"] = {"format": output_format("json_schema")}
return 200, events
async with _harness("responses", reply, max_attempts=3) as (send, bodies, _):
raw = await delivered(
send("responses", [{"role": "user", "content": "hello"}]), "responses"
)
assert len(bodies) == (2 if continuing else 1)
assert parse_sse_text(raw)[-1].event == "response.failed"
assert public_text(parse_sse_text(raw), "responses") == "Hello "
@pytest.mark.asyncio
@pytest.mark.parametrize("constraint", ["structured", "logprobs"])
@pytest.mark.parametrize("later_reasoning", [False, True])
async def test_complete_calls_do_not_bypass_unsupported_text_guard(
constraint, later_reasoning
):
prefix = text_events("responses", '{"value":', complete=False)
if constraint == "logprobs":
prefix = scored_events("Hello ", "delta")[:4]
if later_reasoning:
reasoning = deepcopy(_events_for("responses")[1:-1])
for event in reasoning:
event["output_index"] = 1
prefix += reasoning
calls = deepcopy(tool_events("responses", '{"path":"file"}')[1:-1])
for event in calls:
event["output_index"] += 2 if later_reasoning else 1
async with _harness(
"responses", lambda _: (200, prefix + calls), max_attempts=1
) as (_, bodies, transport):
fields = {"tools": _tools("responses")}
if constraint == "structured":
fields["text"] = {"format": output_format("json_schema")}
raw = await delivered(
responses_stream(transport, "responses", **fields), "responses"
)
events = parse_sse_text(raw)
assert len(bodies) == 1
assert events[-1].event == "response.failed"
assert not any(event.event == "response.output_text.done" for event in events)
@pytest.mark.asyncio
@pytest.mark.parametrize(
"protocol,wire",
[("responses", "messages"), ("chat", "messages"), ("chat", "responses")],
)
async def test_metadata_already_omitted_by_projection_does_not_block_recovery(
protocol, wire
):
def reply(bodies):
if protocol == "responses":
events = scored_events("Hello " if len(bodies) == 1 else "world.", "all")
return 200, events[:4] if len(bodies) == 1 else events
events = text_events(
"chat", "Hello " if len(bodies) == 1 else "world.", complete=len(bodies) > 1
)
text = events[0]["choices"][0]["delta"]["content"]
events[0]["choices"][0]["logprobs"] = {
"content": [
{
"token": text,
"logprob": -0.5,
"bytes": list(text.encode()),
"top_logprobs": [],
}
]
}
return 200, events
async with _harness(protocol, reply, max_attempts=2) as (send, bodies, _):
raw = await delivered(send(wire, [{"role": "user", "content": "hello"}]), wire)
assert len(bodies) == 2
assert_completed(parse_sse_text(raw), wire)
assert public_text(parse_sse_text(raw), wire) == "Hello world."
@pytest.mark.asyncio
@pytest.mark.parametrize("constraint", ["structured", "logprobs"])
async def test_failed_snapshot_keeps_its_error_category_with_text_metadata(constraint):
def reply(bodies):
if len(bodies) == 1:
return 200, text_events("responses", "Hello ", complete=False)
failed = scored_events("unused", "terminal")[-1]
if constraint == "structured":
failed["response"]["output"] = []
failed["response"]["text"] = {"format": output_format("json_schema")}
failed["type"] = "response.failed"
failed["response"]["status"] = "failed"
failed["response"]["error"] = {
"code": "context_length_exceeded",
"message": "The context window is full.",
}
return 200, [failed]
async with _harness("responses", reply, max_attempts=3) as (send, bodies, _):
raw = await delivered(
send("responses", [{"role": "user", "content": "hello"}]), "responses"
)
assert len(bodies) == 2
events = parse_sse_text(raw)
assert events[-1].event == "response.failed"
assert events[-1].data["response"]["error"]["code"] == "context_length_exceeded"
assert public_text(events, "responses") == "Hello "

View file

@ -8,10 +8,10 @@ import pytest
import simplejson
from starlette.responses import StreamingResponse
from free_claude_code.api.handlers.classifier_response import classifier_response
from free_claude_code.api.response_streams import anthropic_sse_streaming_response
from free_claude_code.core.anthropic.stream_contracts import parse_sse_text
from free_claude_code.core.stream_delivery import current_stream_delivery
from tests.api.test_response_streams import _serve
from tests.api.test_response_streams import _json_error, _serve
from tests.api.test_tool_call_buffer import _call, _end, _frames, _response, _start
from tests.api.test_web_server_tools import (
ScriptedSelectionProvider,
@ -57,7 +57,7 @@ async def test_messages_start_is_published_only_with_committed_activity(ending):
response = await _response("messages", source())
try:
iterator = aiter(response.body_iterator)
assert await anext(iterator) == harmless
assert str(await anext(iterator)) + str(await anext(iterator)) == harmless
remaining = "".join([str(chunk) async for chunk in iterator])
if ending in {"activity", "empty", "stop"}:
assert remaining.startswith(start)
@ -251,7 +251,16 @@ async def test_classifier_resets_projection_when_hidden_attempt_is_replaced():
)
)
output = await drained(classifier_response(source()), "messages")
response = await anthropic_sse_streaming_response(
source(),
pre_start_error_response=_json_error,
request_id="classifier-retry",
hide_reasoning=True,
)
assert isinstance(response, StreamingResponse)
output = b"".join(
message.get("body", b"") for message in await _serve(response)
).decode()
assert "hidden" not in output
assert "call_0" in output and "message_stop" in output
assert '"index": 0' in output

View file

@ -129,7 +129,10 @@ async def test_real_transport_cutoff_cannot_complete_partial_tool(protocol, wire
)
async with _harness(
protocol, lambda _: (200, events), chat_provider_factory=chat_provider
protocol,
lambda _: (200, events),
chat_provider_factory=chat_provider,
max_attempts=1,
) as (send, bodies, _):
response = await _response(
wire, send(wire, [{"role": "user", "content": "read"}], tools=_tools(wire))

View file

@ -0,0 +1,252 @@
"""Independent answer partitions exercise the overlap policy and its bounds."""
from copy import deepcopy
import pytest
from free_claude_code.core.continuation_overlap import ContinuationOverlap
from free_claude_code.core.delivered_response import DeliveredResponse
from free_claude_code.core.failures import ExecutionFailure
from tests.api.test_continuation_overlap import responses_parts
@pytest.mark.parametrize("wire", ["messages", "responses"])
def test_empty_delta_stays_invisible_with_no_prior_answer_text(wire):
event = (
{
"type": "content_block_delta",
"index": 0,
"delta": {"type": "text_delta", "text": ""},
}
if wire == "messages"
else {
"type": "response.output_text.delta",
"output_index": 0,
"content_index": 0,
"item_id": "msg_text",
"delta": "",
}
)
assert ContinuationOverlap("").feed(event) == []
def retained_parts(prefix, parts):
text = "".join(parts)
overlap = max(
size
for size in range(min(len(prefix), len(text)) + 1)
if prefix.endswith(text[:size])
)
result = []
for part in parts:
result.append(part[overlap:])
overlap = max(0, overlap - len(part))
return result
@pytest.mark.parametrize(
"prefix,parts",
[
("Hello ", ["He", "llo world."]),
("abcabc", ["", "abc", "", "abcX"]),
("abcabd", ["ab", "cX"]),
("Hello 世界", ["世", "界!"]),
("The answer is", ["answer", " is"]),
("Hello ", ["He", "l"]),
],
)
@pytest.mark.parametrize(
"representation",
[
"deltas",
"text_done",
"part_done",
"item_done",
"terminal",
"part_added",
"item_added",
"mixed",
],
)
@pytest.mark.parametrize("separate_items", [False, True])
def test_text_representation_preserves_each_source_part(
prefix, parts, representation, separate_items
):
source = responses_parts(
parts, representation=representation, separate_items=separate_items
)
original = deepcopy(source)
overlap = ContinuationOverlap(prefix)
events = [ready for event in source for ready in overlap.feed(event)]
expected = retained_parts(prefix, parts)
output = events[-1]["response"]["output"]
assert [part["text"] for item in output for part in item["content"]] == expected
assert source == original
finals = {item["id"]: item for item in output}
deltas = {}
for event in events:
kind = event["type"]
if kind == "response.output_text.delta":
key = (event["item_id"], event["content_index"])
deltas[key] = deltas.get(key, "") + event["delta"]
elif kind == "response.output_text.done":
assert (
event["text"]
== finals[event["item_id"]]["content"][event["content_index"]]["text"]
)
elif kind == "response.content_part.done":
assert (
event["part"]
== finals[event["item_id"]]["content"][event["content_index"]]
)
elif kind == "response.output_item.done":
assert event["item"] == finals[event["item"]["id"]]
if representation == "deltas":
for item in output:
for position, part in enumerate(item["content"]):
assert deltas.get((item["id"], position), "") == part["text"]
def test_pending_snapshots_are_idempotent_and_lifecycle_frames_keep_order():
source = responses_parts(["He", "llo world."], representation="text_done")
source.insert(4, deepcopy(source[3]))
source.insert(5, {"type": "ping", "marker": "between repeated snapshots"})
overlap = ContinuationOverlap("Hello ")
released = []
for index, event in enumerate(source):
ready = overlap.feed(event)
if 3 <= index <= 6:
assert ready == []
released.extend(ready)
assert [event["type"] for event in released] == [event["type"] for event in source]
assert [
part["text"] for part in released[-1]["response"]["output"][0]["content"]
] == ["", "world."]
def test_interleaved_parts_wait_for_the_earlier_open_part():
source = responses_parts(["He", "llo world."], representation="text_done")
earlier_done = source[3:5]
later = source[5:8]
overlap = ContinuationOverlap("Hello ")
released = []
for event in [*source[:3], *later]:
ready = overlap.feed(event)
if event in later and event["type"].endswith(".done"):
assert ready == []
released.extend(ready)
for event in [*earlier_done, *source[8:]]:
released.extend(overlap.feed(event))
assert released[-1]["response"]["output"][0]["content"] == [
{"type": "output_text", "text": "", "annotations": []},
{"type": "output_text", "text": "world.", "annotations": []},
]
assert [
event["content_index"]
for event in released
if event["type"] == "response.output_text.done"
] == [1, 0]
@pytest.mark.parametrize("change", ["prefix", "order", "insert"])
def test_late_snapshot_cannot_rewrite_a_decided_prefix(change):
source = responses_parts(
["He", "llo world."], representation="text_done", separate_items=True
)
overlap = ContinuationOverlap("Hello ")
for event in source[:-1]:
overlap.feed(event)
terminal = source[-1]
items = terminal["response"]["output"]
if change == "prefix":
items[0]["content"][0]["text"] = "Ha"
elif change == "order":
items.reverse()
else:
extra = deepcopy(items[0])
extra["id"] = "msg_late"
items.insert(0, extra)
with pytest.raises(ExecutionFailure, match="observed answer prefix"):
overlap.feed(terminal)
def test_terminal_cannot_omit_pending_source_text():
source = responses_parts(["He"], representation="text_done")
overlap = ContinuationOverlap("Hello ")
for event in source[:-1]:
overlap.feed(event)
source[-1]["response"]["output"] = []
with pytest.raises(ExecutionFailure, match="observed answer prefix"):
overlap.feed(source[-1])
def test_unresolved_frames_are_bounded_and_discarded_on_failure(monkeypatch):
monkeypatch.setattr(
"free_claude_code.core.continuation_overlap.RECOVERY_RETENTION_BYTES", 1000
)
overlap = ContinuationOverlap("Hello ")
source = responses_parts(["He"], representation="text_done")
for event in source[:4]:
overlap.feed(event)
with pytest.raises(ExecutionFailure, match="retained-data limit"):
for _ in range(30):
overlap.feed({"type": "ping", "padding": "x" * 60})
failure = {"type": "response.failed", "response": {"output": []}}
assert overlap.feed(failure) == [failure]
def test_large_immediately_resolvable_snapshot_does_not_fill_the_pending_buffer(
monkeypatch,
):
monkeypatch.setattr(
"free_claude_code.core.continuation_overlap.RECOVERY_RETENTION_BYTES", 300
)
overlap = ContinuationOverlap("Hello ")
source = responses_parts(["Hello " + "x" * 10000], representation="terminal")
events = [ready for event in source for ready in overlap.feed(event)]
assert events[-1]["response"]["output"][0]["content"][0]["text"] == "x" * 10000
def test_resolved_overlap_does_not_retain_unaffected_later_items(monkeypatch):
monkeypatch.setattr(
"free_claude_code.core.continuation_overlap.RECOVERY_RETENTION_BYTES", 400
)
overlap = ContinuationOverlap("Hello ")
source = responses_parts(["Hello world."], representation="text_done")
for event in source[:-1]:
overlap.feed(event)
for index in range(1, 1000):
event = {
"type": "response.output_item.done",
"output_index": index,
"item": {
"id": f"msg_{index}",
"type": "message",
"content": [{"type": "output_text", "text": "More."}],
},
}
assert overlap.feed(event) == [event]
@pytest.mark.parametrize(
"surface", ["item_added", "part_added", "text_done", "part_done", "item_done"]
)
def test_snapshot_only_text_counts_as_new_public_progress_once(surface):
response = DeliveredResponse("responses")
source = responses_parts(["Novel"], representation=surface)
kind = {
"item_added": "response.output_item.added",
"part_added": "response.content_part.added",
"text_done": "response.output_text.done",
"part_done": "response.content_part.done",
"item_done": "response.output_item.done",
}[surface]
target = next(event for event in source if event["type"] == kind)
if surface != "item_added":
response.observe(source[1])
initial = response.progress
response.observe(target)
assert response.progress > initial
released = response.progress
response.observe(target)
assert response.progress == released

View file

@ -0,0 +1,205 @@
"""Published completion stages determine the remaining recovery boundary."""
import pytest
from free_claude_code.core.continuation_stream import ContinuationStream
from free_claude_code.core.delivered_response import DeliveredResponse
@pytest.mark.parametrize("summary", [False, True])
@pytest.mark.parametrize("closed", ["none", "text", "part", "item"])
def test_readable_reasoning_closes_only_missing_stages(summary, closed):
response = DeliveredResponse("responses")
field = "summary" if summary else "content"
part = {"type": "summary_text" if summary else "reasoning_text", "text": "Reason."}
item = {
"type": "reasoning",
"id": "rs_first",
"status": "in_progress",
field: [part],
}
response.observe(
{"type": "response.output_item.added", "output_index": 0, "item": item}
)
identity = {
"output_index": 0,
"item_id": "rs_first",
"summary_index" if summary else "content_index": 0,
}
text_kind = (
"response.reasoning_summary_text.done"
if summary
else "response.reasoning_text.done"
)
part_kind = (
"response.reasoning_summary_part.done"
if summary
else "response.content_part.done"
)
if closed in {"text", "part", "item"}:
response.observe({"type": text_kind, **identity, "text": "Reason."})
if closed in {"part", "item"}:
response.observe({"type": part_kind, **identity, "part": part})
if closed == "item":
response.observe(
{
"type": "response.output_item.done",
"output_index": 0,
"item": {**item, "status": "completed"},
}
)
expected = (
([] if closed != "none" else [text_kind])
+ ([part_kind] if summary and closed in {"none", "text"} else [])
+ ([] if closed == "item" else ["response.output_item.done"])
)
events = response.closing_events()
assert [event["type"] for event in events] == expected
assert response.closing_events() == events
for event in events:
response.observe(event)
assert response.closing_events() == []
def test_finished_part_does_not_require_another_text_done():
response = DeliveredResponse("responses")
part = {"type": "output_text", "text": "Done."}
response.observe(
{
"type": "response.output_item.added",
"output_index": 0,
"item": {"type": "message", "id": "msg_first", "content": [part]},
}
)
response.observe(
{
"type": "response.content_part.done",
"output_index": 0,
"content_index": 0,
"part": part,
}
)
assert [event["type"] for event in response.closing_events()] == [
"response.output_item.done"
]
def test_empty_blocks_and_usage_updates_do_not_finish_overlap():
response = DeliveredResponse("messages")
response.observe(
{
"type": "content_block_start",
"index": 0,
"content_block": {"type": "text", "text": "Hello "},
}
)
continuation = ContinuationStream(response)
continuation.begin(handoff=False)
for event in continuation.boundary():
response.observe(event)
continuation.prepare(
{
"type": "content_block_start",
"index": 0,
"content_block": {"type": "text", "text": ""},
}
)
assert (
continuation.prepare(
{
"type": "content_block_delta",
"index": 0,
"delta": {"type": "text_delta", "text": ""},
}
)
== []
)
continuation.prepare({"type": "content_block_stop", "index": 0})
continuation.prepare(
{
"type": "message_delta",
"delta": {"stop_reason": None},
"usage": {"output_tokens": 1},
}
)
assert (
continuation.prepare(
{
"type": "content_block_delta",
"index": 1,
"delta": {"type": "text_delta", "text": "l"},
}
)
== []
)
events = continuation.prepare(
{
"type": "content_block_delta",
"index": 1,
"delta": {"type": "text_delta", "text": "o world."},
}
)
assert events[0]["delta"]["text"] == "world."
@pytest.mark.parametrize("seed", ["added", "done"])
def test_refusal_content_without_deltas_counts_as_progress(seed):
response = DeliveredResponse("responses")
response.observe(
{
"type": "response.output_item.added",
"output_index": 0,
"item": {"type": "message", "id": "msg_refusal", "content": []},
}
)
initial = response.progress
response.observe(
{
"type": f"response.content_part.{seed}",
"output_index": 0,
"content_index": 0,
"part": {"type": "refusal", "refusal": "Cannot continue."},
}
)
assert response.progress > initial
assert not response.snapshot().eligible
assert not response.can_handoff
def test_handoff_fallback_preserves_already_closed_item_status(monkeypatch):
response = DeliveredResponse("responses")
response.observe(
{
"type": "response.output_item.done",
"output_index": 0,
"item": {
"type": "message",
"id": "msg_closed",
"status": "incomplete",
"content": [],
},
}
)
response.observe(
{
"type": "response.output_item.added",
"output_index": 1,
"item": {
"type": "message",
"id": "msg_open",
"status": "in_progress",
"content": [{"type": "output_text", "text": "Next."}],
},
}
)
continuation = ContinuationStream(response)
continuation.begin(handoff=True)
monkeypatch.setattr(
"free_claude_code.core.delivered_response.RECOVERY_RETENTION_BYTES", 1
)
for event in continuation.boundary():
response.observe(event)
result = continuation.finalize(
continuation.prepare(response.handoff_events()[0])[0]
)["response"]
assert [item["status"] for item in result["output"]] == ["incomplete", "completed"]

View file

@ -0,0 +1,121 @@
"""Public replay carriers retain the recovery restrictions of their native state."""
import pytest
from free_claude_code.core.delivered_response import DeliveredResponse
from free_claude_code.core.history_replay import (
ReplayOrigin,
ReplayRecord,
encode_replay,
)
def carrier(protocol, native):
return encode_replay(
ReplayRecord(ReplayOrigin("test", protocol, "", "", "model"), native)
)
@pytest.mark.parametrize("surface", ["responses", "signature", "redacted"])
@pytest.mark.parametrize(
("value", "eligible"),
[
(carrier("responses", {"type": "reasoning", "summary": []}), True),
(
carrier(
"responses",
{"type": "reasoning", "summary": [], "encrypted_content": None},
),
True,
),
(
carrier(
"responses",
{"type": "reasoning", "summary": [], "encrypted_content": ""},
),
True,
),
(
carrier(
"responses",
{
"type": "reasoning",
"summary": [{"type": "summary_text", "text": "Readable."}],
"encrypted_content": "cipher",
},
),
False,
),
(
carrier(
"messages",
{"type": "thinking", "thinking": "Readable.", "signature": "signed"},
),
False,
),
(
carrier(
"chat",
{
"reasoning_details": [
{"type": "reasoning.text", "text": "Readable."}
]
},
),
False,
),
("native-ciphertext", False),
("fcc:history:v1:malformed", False),
("fcc:history:v99:future", False),
],
ids=[
"absent",
"null",
"empty",
"encrypted",
"signed",
"structured",
"raw",
"malformed",
"unknown-version",
],
)
def test_public_carriers_follow_native_reasoning_semantics(surface, value, eligible):
delivered = DeliveredResponse("responses" if surface == "responses" else "messages")
if surface == "responses":
delivered.observe(
{
"type": "response.output_item.added",
"output_index": 0,
"item": {
"type": "reasoning",
"id": "rs_public",
"summary": [],
"encrypted_content": value,
},
}
)
elif surface == "signature":
delivered.observe(
{
"type": "content_block_start",
"index": 0,
"content_block": {"type": "thinking", "thinking": "Readable."},
}
)
delivered.observe(
{
"type": "content_block_delta",
"index": 0,
"delta": {"type": "signature_delta", "signature": value},
}
)
else:
delivered.observe(
{
"type": "content_block_start",
"index": 0,
"content_block": {"type": "redacted_thinking", "data": value},
}
)
assert delivered.snapshot().eligible is eligible

View file

@ -0,0 +1,69 @@
"""Continuation tails are replaceable without rolling back accepted corrections."""
from copy import deepcopy
import pytest
from free_claude_code.providers.continuation import ContinuationRequest
@pytest.mark.parametrize("protocol", ["chat", "messages", "responses"])
@pytest.mark.parametrize("initial_text", ["", "First "])
def test_next_continuation_preserves_corrected_history_and_options(
protocol, initial_text
):
field = "input" if protocol == "responses" else "messages"
user_turn = {"role": "user", "content": "Continue"}
body = {
field: [
{"role": "assistant", "content": "An old answer"},
user_turn,
],
"max_tokens": 100,
"reasoning": {"effort": "none"},
"extra_body": {"retained": True, "rejected": True},
"temperature": 0.5,
}
original = deepcopy(body)
request = ContinuationRequest(protocol)
first = request.build(body, initial_text, "First thought.")
assert body == original
corrected = deepcopy(first)
# An accepted correction can remove a historical row and change request options.
del corrected[field][0]
del corrected["reasoning"]
del corrected["extra_body"]["rejected"]
del corrected["temperature"]
corrected["max_tokens"] = 200
before = deepcopy(corrected)
second = request.build(corrected, "First answer.", "First thought. Second thought.")
assert corrected == before
assert second[field][:-2] == [user_turn]
assert second["max_tokens"] == 200
assert second["extra_body"] == {"retained": True}
assert "reasoning" not in second and "temperature" not in second
assert "First answer." in str(second[field][-2])
assert "First thought. Second thought." in str(second[field][-1])
assert request.build(second, "First answer. More.", "")[field][:-2] == [user_turn]
@pytest.mark.parametrize("protocol", ["chat", "messages", "responses"])
def test_original_turns_matching_private_instruction_are_preserved(protocol):
field = "input" if protocol == "responses" else "messages"
instruction_turn = ContinuationRequest(protocol).build({field: []}, "", "")[field][
-1
]
body = {field: [instruction_turn, {"role": "user", "content": "Continue"}]}
original = deepcopy(body)
request = ContinuationRequest(protocol)
first = request.build(body, "One", "")
second = request.build(first, "One two", "")
assert second[field][:-2] == original[field]
assert body == original
def test_responses_string_input_is_preserved_across_continuations():
request = ContinuationRequest("responses")
first = request.build({"input": "Original prompt"}, "", "A thought.")
second = request.build(first, "Answer", "A thought.")
assert second["input"][:-2] == [{"role": "user", "content": "Original prompt"}]

View file

@ -286,7 +286,9 @@ async def test_hidden_normal_stop_prevents_request_corrections(
if public:
options = {"pre_start_error_response": _json_error, "request_id": "stopped"}
response = await (
anthropic_sse_streaming_response(source, **options)
anthropic_sse_streaming_response(
source, pre_start_error_response=_json_error, request_id="stopped"
)
if wire == "messages"
else openai_responses_sse_streaming_response(
source, headers={}, **options

View file

@ -379,7 +379,9 @@ async def test_stopped_first_nim_chunk_returns_public_error_and_releases_admissi
source = _public_stream(provider, wire)
async with asyncio.timeout(2):
response = await (
anthropic_sse_streaming_response(source, **options)
anthropic_sse_streaming_response(
source, pre_start_error_response=_json_error, request_id="raw-stop"
)
if wire == "messages"
else openai_responses_sse_streaming_response(
source, headers={}, **options