chore(deps): repin worldtree-sdk 1.1.2 → 1.2.0 + catch ResponseTooLarge
1.2.0 adds response-side allocation caps + a new exported `ResponseTooLarge` (a ProtocolError, NOT a ConnectionDropped — retrying an oversized response is futile; caps: 2xx body 108,004,096 B, SSE frame 540,000 B, both calibrated to a 2.5.0 server, which worldtree-dev runs, so safe on legal traffic). Absorbed WT spec 2.4.0/2.5.0 (zero-schema, no generated-type change). We catalogue SDK errors explicitly, so ResponseTooLarge needs a home: added `SseResponseTooLarge` (sse_client), mapped from `wtsdk.ResponseTooLarge` in `wt.stream_turn` + `wt.stream_admin_events` (above the ApiError default — it's a ProtocolError, not an ApiError), and caught in the two stream endpoints so an oversized SSE frame surfaces as a labeled error, never an uncaught 500 or a futile reconnect. The 108MB read-body cap is unreachable on legal traffic (a 108-megabyte transcript page is absurd), so reads inherit the SDK refusal unwrapped. +2 adapter-mapping tests; 548 green. Done during the DCC-fix wait.
This commit is contained in:
@@ -138,9 +138,11 @@ adoption** (ruled normative, not blocking → `persistent-memory.d/2026-07-16-bi
|
||||
**Substrate / environment:** branch `main`, **HEAD `d59f907` PUSHED to origin** (v0.22.0 cutover + bifrost 1.1.5
|
||||
v0.22.1 + this session's TTS work all on origin; nothing local-only but the dirty `graphify-out/`). origin
|
||||
`git@gitea.phasefinal.com:vh/ratatoskr.git`. **Core dep: `worldtree-sdk==1.0.0`** (gitea PyPI, `[tool.uv.sources]`;
|
||||
`httpx-sse` removed — SDK owns SSE). **worldtree-sdk 1.2.0 is AVAILABLE (repin DEFERRED to a dep pass, my rec) —**
|
||||
new `ResponseTooLarge` (a `ProtocolError`, NOT caught by our `except ConnectFailed`) + response-alloc caps that assume
|
||||
a 2.5.0 server (worldtree-dev runs it, so safe); wtsdk-dev announce `01KZ1ZYM…`. bifrost **`==1.1.5`** / wire v0.7; WT
|
||||
`httpx-sse` removed — SDK owns SSE). **worldtree-sdk REPINNED 1.1.2→1.2.0 (operator-directed, during the DCC-fix
|
||||
wait) — new `ResponseTooLarge` (a `ProtocolError`) mapped → `SseResponseTooLarge` at the stream surfaces
|
||||
(`wt.stream_turn`/`stream_admin_events` + the 2 stream endpoints); the 108MB read-body cap is unreachable on legal
|
||||
traffic so reads inherit the SDK refusal unwrapped. FOOT-GUN: this project's dev deps are a `[project.optional-
|
||||
dependencies]` EXTRA — `uv sync` alone PRUNES pytest/respx; use `uv sync --all-extras`. bifrost **`==1.1.5`** / wire v0.7; WT
|
||||
openapi vendored 2.3.0, conversation-api-spec v1.1; **suite 546 green.** Personal WT on **b128**
|
||||
(`http://10.250.50.152:8081`; #368 silo + #364 promotion-hygiene live both instances). The combined
|
||||
**:8392** provider (memory+affect) + **:8765** web are THE surfaces, dev-box BACKGROUND SHELLS —
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ keywords = ["worldtree", "debug", "sse", "web", "observability"]
|
||||
# owned by worldtree-sdk post-cutover (#20 slice-7 dropped httpx-sse).
|
||||
dependencies = [
|
||||
"httpx>=0.27",
|
||||
"worldtree-sdk==1.1.2", # #20 cutover: the consumer client layer (gitea PyPI); the hand-rolled wrappers now live behind ratatoskr.wt. 1.1.2 = closes the SSE turn-stream traversal we caught in 1.1.1 (quote safe="" at turn_stream.py:185; the _build_path fix had missed that inline construction). 1.1.1 = path-encoding correctness fix (quote safe="" — a caller-shaped `/`/`../` in a path param no longer traverses the URL; we pass caller-shaped session_id/agent_id) + additive define_or_reuse / set_persona_state_raw (our parity #6) + 64 KiB error-body alloc cap. Same frozen wire as 1.0.0 (drop-in).
|
||||
"worldtree-sdk==1.2.0", # #20 cutover: the consumer client layer (gitea PyPI); the hand-rolled wrappers now live behind ratatoskr.wt. 1.2.0 = response-side allocation caps + a new exported `ResponseTooLarge` (a ProtocolError, NOT a ConnectionDropped — retrying an oversized response is futile; caps: 2xx body 108,004,096 B, SSE frame 540,000 B; the caps assume a 2.5.0 server, which worldtree-dev runs, so safe on legal traffic). We catch it explicitly at the stream + read surfaces (INV-CUT-2). Absorbed WT spec 2.4.0/2.5.0 (zero-schema, no generated-type change). 1.1.2 = closed the SSE turn-stream traversal we caught in 1.1.1 (quote safe="" at turn_stream.py:185). 1.1.1 = path-encoding correctness fix + additive define_or_reuse / set_persona_state_raw (our parity #6) + 64 KiB error-body alloc cap. Same frozen wire as 1.0.0 (drop-in).
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
@@ -122,6 +122,19 @@ class SseConnectionDropped(Exception):
|
||||
self.last_seen_sse_id = last_seen_sse_id
|
||||
|
||||
|
||||
class SseResponseTooLarge(Exception):
|
||||
"""Raised when the SDK refuses an oversized SSE frame (worldtree-sdk 1.2.0
|
||||
`ResponseTooLarge`, scope="sse_frame"). Distinct from SseConnectionDropped: the SDK
|
||||
deliberately does NOT class it resumable — retrying yields the same oversized frame —
|
||||
so callers surface it as a terminal error, never a reconnect. `limit_bytes` is the
|
||||
ceiling the frame exceeded."""
|
||||
|
||||
def __init__(self, *, limit_bytes: int, scope: str = "sse_frame") -> None:
|
||||
super().__init__(f"SSE {scope} exceeded {limit_bytes} bytes")
|
||||
self.limit_bytes = limit_bytes
|
||||
self.scope = scope
|
||||
|
||||
|
||||
class InvalidLastEventId(Exception):
|
||||
"""Raised on HTTP 400 from a reconnect request — caller's last_event_id was rejected."""
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ from ratatoskr.sse_client import (
|
||||
MalformedSseId,
|
||||
SseConnectFailed,
|
||||
SseConnectionDropped,
|
||||
SseResponseTooLarge,
|
||||
TurnIdFlip,
|
||||
)
|
||||
from ratatoskr.tts import (
|
||||
@@ -386,7 +387,7 @@ async def _stream_turn_endpoint(request: Request) -> StreamingResponse:
|
||||
handle.status = event.type or "done"
|
||||
break
|
||||
except (wt.SessionApiFailed, SseConnectFailed, SseConnectionDropped,
|
||||
MalformedSseId, MalformedSseData, TurnIdFlip) as exc:
|
||||
SseResponseTooLarge, MalformedSseId, MalformedSseData, TurnIdFlip) as exc:
|
||||
# wt.SessionApiFailed covers the adapter's SessionRetired (410) mapping;
|
||||
# without it a retired-session stream would escape gen() after partial
|
||||
# frames as an uncaught 500, not a labeled `event: error`.
|
||||
@@ -778,8 +779,8 @@ async def _admin_events_endpoint(request: Request) -> Response:
|
||||
{"id": ev.id, "type": ev.type, "timestamp": ev.timestamp,
|
||||
"data": ev.data},
|
||||
)
|
||||
except (SseConnectFailed, SseConnectionDropped, MalformedSseId,
|
||||
MalformedSseData) as exc:
|
||||
except (SseConnectFailed, SseConnectionDropped, SseResponseTooLarge,
|
||||
MalformedSseId, MalformedSseData) as exc:
|
||||
yield _format_sse(
|
||||
"stream_error",
|
||||
{"exception": type(exc).__name__, "message": str(exc)},
|
||||
|
||||
@@ -69,6 +69,7 @@ from .sse_client import (
|
||||
MalformedSseId,
|
||||
SseConnectFailed,
|
||||
SseConnectionDropped,
|
||||
SseResponseTooLarge,
|
||||
TurnIdFlip,
|
||||
TurnLaunchUnavailable,
|
||||
)
|
||||
@@ -336,6 +337,12 @@ async def stream_turn(
|
||||
raise MalformedSseData(raw=exc.raw) from exc
|
||||
except wtsdk.TurnIdFlip as exc:
|
||||
raise TurnIdFlip(established=exc.established, got=exc.got) from exc
|
||||
except wtsdk.ResponseTooLarge as exc:
|
||||
# 1.2.0: the SDK refused an oversized SSE frame — a ProtocolError, NOT a
|
||||
# ConnectionDropped (retrying yields the same frame), so surface as a terminal
|
||||
# error the endpoint labels, never a reconnect. Placed above the ApiError default
|
||||
# because ResponseTooLarge is a ProtocolError, not an ApiError.
|
||||
raise SseResponseTooLarge(limit_bytes=exc.limit_bytes, scope=exc.scope) from exc
|
||||
except ApiError as exc:
|
||||
# INV-CUT-2 default: an undiscriminated ApiError surfacing from the stream →
|
||||
# SessionApiFailed (the discriminated stream errors are handled above).
|
||||
@@ -764,6 +771,10 @@ async def stream_admin_events(
|
||||
# stream_turn. The web gen catches the Sse* types, so an unmapped ConnectFailed
|
||||
# would escape and abort the SSE with no labeled stream_error (heid bug-hunt).
|
||||
raise SseConnectFailed(status=exc.status, body=(exc.message or "").encode()) from exc
|
||||
except wtsdk.ResponseTooLarge as exc:
|
||||
# 1.2.0: an oversized admin SSE frame refused by the SDK (ProtocolError, non-resumable)
|
||||
# → surface as a terminal error, mirroring stream_turn (never a reconnect).
|
||||
raise SseResponseTooLarge(limit_bytes=exc.limit_bytes, scope=exc.scope) from exc
|
||||
except ApiError as exc:
|
||||
# A non-200 open raises ApiError("admin_stream_failed", status=…) → SseConnectFailed.
|
||||
raise SseConnectFailed(status=exc.status, body=(exc.body or "").encode()) from exc
|
||||
|
||||
@@ -47,6 +47,7 @@ from ratatoskr.sse_client import (
|
||||
MalformedSseId,
|
||||
SseConnectFailed,
|
||||
SseConnectionDropped,
|
||||
SseResponseTooLarge,
|
||||
TurnIdFlip,
|
||||
TurnLaunchUnavailable,
|
||||
)
|
||||
@@ -431,6 +432,16 @@ class TestStreamTurn:
|
||||
await _drain(stream_turn(_wt(fake), "s", "hi"))
|
||||
assert (ei.value.established, ei.value.got) == (5, 7)
|
||||
|
||||
async def test_response_too_large_maps_to_sse_response_too_large(self) -> None:
|
||||
# 1.2.0: an oversized SSE frame (ProtocolError, NOT ConnectionDropped) → a terminal
|
||||
# SseResponseTooLarge, never a resumable drop. Carries limit_bytes + scope.
|
||||
fake = _FakeSessions(
|
||||
stream_error=wtsdk.ResponseTooLarge(limit_bytes=540000, scope="sse_frame")
|
||||
)
|
||||
with pytest.raises(SseResponseTooLarge) as ei:
|
||||
await _drain(stream_turn(_wt(fake), "s", "hi"))
|
||||
assert ei.value.limit_bytes == 540000 and ei.value.scope == "sse_frame"
|
||||
|
||||
async def test_undiscriminated_api_error_maps_to_session_api_failed(self) -> None:
|
||||
# INV-CUT-2 default: an undiscriminated ApiError surfacing from the stream
|
||||
# (not a discriminated stream error) → SessionApiFailed.
|
||||
@@ -1243,6 +1254,16 @@ class TestStreamAdminEventsWt:
|
||||
await _drain(stream_admin_events(_wtad(fake)))
|
||||
assert ei.value.last_seen_sse_id == "42"
|
||||
|
||||
async def test_response_too_large_maps_to_sse_response_too_large(self) -> None:
|
||||
# 1.2.0: an oversized admin SSE frame (ProtocolError) → terminal SseResponseTooLarge,
|
||||
# mirroring stream_turn (never a resumable drop).
|
||||
fake = _FakeAdmin(
|
||||
stream_error=wtsdk.ResponseTooLarge(limit_bytes=540000, scope="sse_frame")
|
||||
)
|
||||
with pytest.raises(SseResponseTooLarge) as ei:
|
||||
await _drain(stream_admin_events(_wtad(fake)))
|
||||
assert ei.value.limit_bytes == 540000
|
||||
|
||||
async def test_connection_dropped_none_cursor_connect_time(self) -> None:
|
||||
# A connect-time transport failure surfaces as ConnectionDropped(None) →
|
||||
# SseConnectionDropped(last_seen_sse_id=None) (the map's other cursor shape;
|
||||
|
||||
@@ -509,7 +509,7 @@ requires-dist = [
|
||||
{ name = "sqlite-vec", marker = "extra == 'provider'", specifier = ">=0.1.6" },
|
||||
{ name = "starlette", marker = "extra == 'web'", specifier = ">=0.40" },
|
||||
{ name = "uvicorn", extras = ["standard"], marker = "extra == 'web'", specifier = ">=0.30" },
|
||||
{ name = "worldtree-sdk", specifier = "==1.1.2", index = "https://gitea.phasefinal.com/api/packages/vh/pypi/simple/" },
|
||||
{ name = "worldtree-sdk", specifier = "==1.2.0", index = "https://gitea.phasefinal.com/api/packages/vh/pypi/simple/" },
|
||||
]
|
||||
provides-extras = ["web", "provider", "dev"]
|
||||
|
||||
@@ -897,12 +897,12 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "worldtree-sdk"
|
||||
version = "1.1.2"
|
||||
version = "1.2.0"
|
||||
source = { registry = "https://gitea.phasefinal.com/api/packages/vh/pypi/simple/" }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
]
|
||||
sdist = { url = "https://gitea.phasefinal.com/api/packages/vh/pypi/files/worldtree-sdk/1.1.2/worldtree_sdk-1.1.2.tar.gz", hash = "sha256:bd1eb8b303d4dd28ddb1fd63faac61a2674521f57f6bfdf580496245d75f08c2" }
|
||||
sdist = { url = "https://gitea.phasefinal.com/api/packages/vh/pypi/files/worldtree-sdk/1.2.0/worldtree_sdk-1.2.0.tar.gz", hash = "sha256:48b1a9f44875069aade079f999c72c8df4cc7c6abf2e49fb9ad52a067594bd19" }
|
||||
wheels = [
|
||||
{ url = "https://gitea.phasefinal.com/api/packages/vh/pypi/files/worldtree-sdk/1.1.2/worldtree_sdk-1.1.2-py3-none-any.whl", hash = "sha256:81161a4ee58e45a6b2fa6044421a4baa56f25a0d26dca9b5f4a5113ce51835b2" },
|
||||
{ url = "https://gitea.phasefinal.com/api/packages/vh/pypi/files/worldtree-sdk/1.2.0/worldtree_sdk-1.2.0-py3-none-any.whl", hash = "sha256:3516841dad7adf7a7bef5a492d2c9fe0b3e7abc3c453ee573ceddf20f7e6823a" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user