mirror of
https://github.com/Alishahryar1/free-claude-code.git
synced 2026-10-19 07:23:36 +00:00
Some checks are pending
## 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"> **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 -->
412 lines
13 KiB
Python
412 lines
13 KiB
Python
"""FastAPI streaming response wrappers for public API wire formats."""
|
|
|
|
import asyncio
|
|
from collections.abc import (
|
|
AsyncIterator,
|
|
Awaitable,
|
|
Callable,
|
|
Mapping,
|
|
)
|
|
from typing import Literal
|
|
|
|
from fastapi.responses import JSONResponse, Response, StreamingResponse
|
|
from loguru import logger
|
|
from starlette.background import BackgroundTask
|
|
from starlette.responses import ContentStream
|
|
from starlette.types import Receive, Scope, Send
|
|
|
|
from free_claude_code.core.anthropic import anthropic_error_type_for_failure
|
|
from free_claude_code.core.anthropic.streaming import (
|
|
ANTHROPIC_SSE_RESPONSE_HEADERS,
|
|
anthropic_terminal_error_frame,
|
|
anthropic_terminal_failure_frame,
|
|
)
|
|
from free_claude_code.core.async_iterators import try_close_async_iterator
|
|
from free_claude_code.core.diagnostics import safe_exception_message
|
|
from free_claude_code.core.failures import find_execution_failure
|
|
from free_claude_code.core.json_types import JsonObject
|
|
from free_claude_code.core.openai_responses import (
|
|
committed_response_failure_frame,
|
|
openai_error_type_for_failure,
|
|
)
|
|
from free_claude_code.core.request_outcomes import (
|
|
record_request_exception,
|
|
record_request_failure,
|
|
)
|
|
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, PublicResponseStream
|
|
|
|
TERMINAL_EXECUTION_ERROR_HEADERS = {"x-should-retry": "false"}
|
|
|
|
PreStartErrorResponse = Callable[[BaseException], Response]
|
|
TerminalFrameEmitter = Callable[[str, str, BaseException], str]
|
|
TerminalFailureObserver = Callable[[BaseException], None]
|
|
ReleaseResponseResource = Callable[[], Awaitable[None]]
|
|
WireApi = Literal["messages", "responses"]
|
|
|
|
|
|
class EmptyStreamError(RuntimeError):
|
|
"""Raised when a public stream ends before emitting any protocol chunk."""
|
|
|
|
|
|
class ManagedStreamingResponse(StreamingResponse):
|
|
"""Own body closure and one response-scoped runtime release callback."""
|
|
|
|
def __init__(
|
|
self,
|
|
content: ContentStream,
|
|
status_code: int = 200,
|
|
headers: Mapping[str, str] | None = None,
|
|
media_type: str | None = None,
|
|
background: BackgroundTask | None = None,
|
|
) -> None:
|
|
super().__init__(
|
|
content,
|
|
status_code=status_code,
|
|
headers=headers,
|
|
media_type=media_type,
|
|
background=background,
|
|
)
|
|
self._release: ReleaseResponseResource | None = None
|
|
self._cleanup_task: asyncio.Task[None] | None = None
|
|
|
|
def bind_release(self, release: ReleaseResponseResource) -> None:
|
|
"""Bind the resource retained for this response before ASGI execution."""
|
|
if self._release is not None:
|
|
raise RuntimeError("A response resource release is already bound.")
|
|
if self._cleanup_task is not None:
|
|
raise RuntimeError("Cannot bind a resource after response cleanup started.")
|
|
self._release = release
|
|
|
|
async def aclose(self) -> None:
|
|
"""Close the body and release its runtime resource exactly once."""
|
|
await self._close(preserved_error=None)
|
|
|
|
async def _close(self, *, preserved_error: BaseException | None) -> None:
|
|
task = self._cleanup_task
|
|
if task is None:
|
|
task = asyncio.create_task(
|
|
self._cleanup(preserved_error=preserved_error),
|
|
name="fcc-api-response-cleanup",
|
|
)
|
|
self._cleanup_task = task
|
|
await _wait_for_cleanup(task)
|
|
|
|
async def __call__(
|
|
self,
|
|
scope: Scope,
|
|
receive: Receive,
|
|
send: Send,
|
|
) -> None:
|
|
preserved_error: BaseException | None = None
|
|
try:
|
|
await super().__call__(scope, receive, send)
|
|
except BaseException as exc:
|
|
preserved_error = exc
|
|
raise
|
|
finally:
|
|
await self._close(preserved_error=preserved_error)
|
|
|
|
async def _cleanup(self, *, preserved_error: BaseException | None) -> None:
|
|
try:
|
|
await close_stream_input(
|
|
self.body_iterator,
|
|
owner="ManagedStreamingResponse",
|
|
source="api",
|
|
preserved_error=preserved_error,
|
|
)
|
|
except Exception as exc:
|
|
_log_response_cleanup_failure("close_body", exc)
|
|
|
|
release = self._release
|
|
if release is None:
|
|
return
|
|
try:
|
|
await release()
|
|
except Exception as exc:
|
|
_log_response_cleanup_failure("release_resource", exc)
|
|
|
|
|
|
async def _wait_for_cleanup(task: asyncio.Task[None]) -> None:
|
|
"""Wait through repeated caller cancellation, then restore cancellation."""
|
|
cancellation: asyncio.CancelledError | None = None
|
|
while not task.done():
|
|
try:
|
|
await asyncio.shield(task)
|
|
except asyncio.CancelledError as exc:
|
|
cancellation = exc
|
|
|
|
# Ordinary cleanup failures preserve the response; cancellation remains control flow.
|
|
try:
|
|
task.result()
|
|
except asyncio.CancelledError:
|
|
if cancellation is not None:
|
|
raise cancellation from None
|
|
raise
|
|
except Exception as exc:
|
|
_log_response_cleanup_failure("cleanup_task", exc)
|
|
|
|
if cancellation is not None:
|
|
raise cancellation
|
|
|
|
|
|
def _log_response_cleanup_failure(operation: str, exc: BaseException) -> None:
|
|
logger.bind(
|
|
stage="egress",
|
|
event="free_claude_code.api.response.cleanup_failed",
|
|
source="api",
|
|
operation=operation,
|
|
exc_type=type(exc).__name__,
|
|
).opt(exception=exc).warning("Response cleanup failed")
|
|
|
|
|
|
async def bind_response_lifetime(
|
|
response: object,
|
|
release: ReleaseResponseResource,
|
|
) -> object:
|
|
"""Retain a runtime resource until a response body is fully consumed."""
|
|
if isinstance(response, ManagedStreamingResponse):
|
|
response.bind_release(release)
|
|
return response
|
|
if isinstance(response, StreamingResponse):
|
|
error = TypeError("Streaming API responses must use ManagedStreamingResponse.")
|
|
try:
|
|
await close_stream_input(
|
|
response.body_iterator,
|
|
owner="bind_response_lifetime",
|
|
source="api",
|
|
preserved_error=error,
|
|
)
|
|
finally:
|
|
await release()
|
|
raise error
|
|
await release()
|
|
return response
|
|
|
|
|
|
def terminal_execution_error_response(
|
|
*, status_code: int, content: JsonObject
|
|
) -> JSONResponse:
|
|
"""Return a final provider-execution error without enabling client retries."""
|
|
return JSONResponse(
|
|
status_code=status_code,
|
|
content=content,
|
|
headers=dict(TERMINAL_EXECUTION_ERROR_HEADERS),
|
|
)
|
|
|
|
|
|
def trace_terminal_execution_error(
|
|
*,
|
|
wire_api: WireApi,
|
|
request_id: str,
|
|
status_code: int,
|
|
error_type: str,
|
|
error: BaseException | None = None,
|
|
) -> None:
|
|
"""Record one correlated terminal-execution decision at the HTTP boundary."""
|
|
if error is not None:
|
|
record_request_exception(error)
|
|
else:
|
|
record_request_failure(error_type)
|
|
fields: dict[str, object] = {
|
|
"stage": "egress",
|
|
"event": "free_claude_code.api.response.terminal_execution_error",
|
|
"source": "api",
|
|
"wire_api": wire_api,
|
|
"request_id": request_id,
|
|
"status_code": status_code,
|
|
"error_type": error_type,
|
|
"client_should_retry": False,
|
|
}
|
|
failure = find_execution_failure(error) if error is not None else None
|
|
if error is not None:
|
|
fields["exc_type"] = type(failure or error).__name__
|
|
if failure is not None:
|
|
fields["failure_kind"] = failure.kind.value
|
|
fields["provider_retryable"] = failure.retryable
|
|
trace_event(**fields)
|
|
|
|
|
|
async def _first_chunk_streaming_response(
|
|
body: AsyncIterator[str],
|
|
*,
|
|
wire_api: WireApi,
|
|
headers: Mapping[str, str],
|
|
pre_start_error_response: PreStartErrorResponse,
|
|
terminal_frame: TerminalFrameEmitter | None,
|
|
terminal_failure_observer: TerminalFailureObserver | None,
|
|
hide_reasoning: bool = False,
|
|
) -> Response:
|
|
state = StreamDeliveryState()
|
|
body = DeliveryObservedStream(body, state)
|
|
try:
|
|
first_chunk = await anext(body)
|
|
except StopAsyncIteration:
|
|
error = EmptyStreamError("Stream ended before emitting a response.")
|
|
await _close_pre_start_body(body, preserved_error=error)
|
|
return pre_start_error_response(error)
|
|
except GeneratorExit as exc:
|
|
await _close_pre_start_body(body, preserved_error=exc)
|
|
raise
|
|
except asyncio.CancelledError as exc:
|
|
await _close_pre_start_body(body, preserved_error=exc)
|
|
raise
|
|
except BaseExceptionGroup as exc:
|
|
await _close_pre_start_body(body, preserved_error=exc)
|
|
return pre_start_error_response(exc)
|
|
except Exception as exc:
|
|
await _close_pre_start_body(body, preserved_error=exc)
|
|
return pre_start_error_response(exc)
|
|
|
|
return ManagedStreamingResponse(
|
|
PublicResponseStream(
|
|
_PrefetchedStream(first_chunk, body),
|
|
state,
|
|
wire_api=wire_api,
|
|
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),
|
|
)
|
|
|
|
|
|
async def _close_pre_start_body(
|
|
body: AsyncIterator[str],
|
|
*,
|
|
preserved_error: BaseException,
|
|
) -> None:
|
|
task = asyncio.create_task(
|
|
close_stream_input(
|
|
body,
|
|
owner="first_chunk_streaming_response",
|
|
source="api",
|
|
preserved_error=preserved_error,
|
|
),
|
|
name="fcc-api-pre-start-stream-cleanup",
|
|
)
|
|
await _wait_for_cleanup(task)
|
|
|
|
|
|
class _PrefetchedStream(AsyncIterator[str]):
|
|
"""Replay the raw prefetched chunk and retain ownership of the tail."""
|
|
|
|
def __init__(self, first_chunk: str, body: AsyncIterator[str]) -> None:
|
|
self._first_chunk: str | None = first_chunk
|
|
self._body = body
|
|
self._closed = False
|
|
|
|
def __aiter__(self) -> _PrefetchedStream:
|
|
return self
|
|
|
|
async def __anext__(self) -> str:
|
|
if self._closed:
|
|
raise StopAsyncIteration
|
|
if self._first_chunk is not None:
|
|
first_chunk, self._first_chunk = self._first_chunk, None
|
|
return first_chunk
|
|
return await anext(self._body)
|
|
|
|
async def aclose(self) -> None:
|
|
if self._closed:
|
|
return
|
|
self._closed = True
|
|
close_error = await try_close_async_iterator(self._body)
|
|
if close_error is not None:
|
|
raise close_error
|
|
|
|
|
|
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(
|
|
exc,
|
|
request_id=request_id,
|
|
),
|
|
)
|
|
|
|
|
|
def _anthropic_terminal_frame(
|
|
_first_chunk: str,
|
|
_latest_chunk: str,
|
|
exc: BaseException,
|
|
) -> str:
|
|
failure = find_execution_failure(exc)
|
|
if failure is not None:
|
|
return anthropic_terminal_failure_frame(failure)
|
|
return anthropic_terminal_error_frame(safe_exception_message(exc))
|
|
|
|
|
|
def _trace_anthropic_terminal_failure(
|
|
exc: BaseException,
|
|
*,
|
|
request_id: str,
|
|
) -> None:
|
|
failure = find_execution_failure(exc)
|
|
trace_terminal_execution_error(
|
|
wire_api="messages",
|
|
request_id=request_id,
|
|
status_code=failure.status_code if failure is not None else 500,
|
|
error_type=(
|
|
anthropic_error_type_for_failure(failure)
|
|
if failure is not None
|
|
else "api_error"
|
|
),
|
|
error=exc,
|
|
)
|
|
|
|
|
|
async def openai_responses_sse_streaming_response(
|
|
body: AsyncIterator[str],
|
|
*,
|
|
headers: Mapping[str, str],
|
|
pre_start_error_response: PreStartErrorResponse,
|
|
request_id: str,
|
|
) -> Response:
|
|
"""Return a streaming response for OpenAI Responses-style SSE."""
|
|
return await _first_chunk_streaming_response(
|
|
body,
|
|
wire_api="responses",
|
|
headers=headers,
|
|
pre_start_error_response=pre_start_error_response,
|
|
terminal_frame=committed_response_failure_frame,
|
|
terminal_failure_observer=lambda exc: _trace_responses_terminal_failure(
|
|
exc,
|
|
request_id=request_id,
|
|
),
|
|
)
|
|
|
|
|
|
def _trace_responses_terminal_failure(
|
|
exc: BaseException,
|
|
*,
|
|
request_id: str,
|
|
) -> None:
|
|
failure = find_execution_failure(exc)
|
|
trace_terminal_execution_error(
|
|
wire_api="responses",
|
|
request_id=request_id,
|
|
status_code=failure.status_code if failure is not None else 500,
|
|
error_type=(
|
|
openai_error_type_for_failure(failure)
|
|
if failure is not None
|
|
else "api_error"
|
|
),
|
|
error=exc,
|
|
)
|