You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Executor-backed responses can fail after only 7,655 bytes of answer text because the request's fixed 1 MiB cumulative response budget counts serialized SSE envelopes, every delta, and repeated done/terminal snapshots. The same 8,000-byte answer succeeds when sent with coarser deltas or as non-streaming JSON. A fast, continuously draining client does not prevent the failure.
This is still reproducible at main commit 2509342dcbc12b89350cabdda17f7bb1a6b8a41b, including its new WebSocket store: false session path. It affects the shared executor, not only WebSocket transport. The fixed upstream SSE-line and JSON-body ceilings are separate constraints.
The resource protections added in #240 are useful, but lifetime wire-byte accounting makes ordinary response capacity depend on chunking and repeated representations. This report proposes separating configurable retained-response, upstream-wire, and client-event limits while keeping bounded queues and cancellation.
Steps to reproduce
The following deterministic upstream uses Python's standard library. It supplies an ordinary Responses lifecycle with deltas and matching full done/terminal snapshots. It requires no model, GPU, SDK, or external service.
Save the script below as public_repro.py, then run python public_repro.py server.
In a separate terminal, build/run the gateway from the selected checkout with its locked dependencies:
Run python public_repro.py client. It requests the same 8,000-byte answer with one-byte and 1,024-byte chunks, then a 220,000-byte answer with 1,024-byte chunks.
Use an isolated config/home and database; the recorded harness clears unrelated gateway/provider environment overrides. On Windows, use the native-build caveat in Environment below. The fixture bytes in this reduced script were checked against the fixture bytes used by the full HTTP/SSE/WebSocket harness.
Self-contained synthetic upstream and HTTP SSE client
Additional controls in the full harness: non-streaming JSON, HTTP pass-through with store: false, WebSocket response.create, exact JSON-body boundary and boundary + 1, and a healthy request on the same WebSocket after a failed request.
Expected behavior
An otherwise identical retained response should not fail solely because the upstream sends more, smaller deltas or repeats the same text in completion snapshots.
Keep independent, configurable limits on actual retained response data, each upstream JSON body/SSE line, and each client stream event. Keep shared limits across inference rounds, gateway tool results, and MCP discovery, plus bounded queues/backpressure.
Raising one allowance must not silently disable the others. Exceeding a resource limit should produce an explicit resource-specific error, terminate the affected request, and avoid publishing a successful durable response or transient checkpoint.
The same 14-case fixture set was run against these source revisions:
Build
Completed
Size failures
3bb55fdf55ea1a7e756cf79694b187909faf62b7, parent of #240
14
0
e87eeae1a8ca09156ececbf18d04487d80324701
8
6
2509342dcbc12b89350cabdda17f7bb1a6b8a41b, including #257
8
6
Local response-resource patch applied on 2509342
14
0
The six baseline failures are four HTTP cases (fine SSE, repeated-snapshot SSE, oversized SSE line, JSON body above the fixed cap) and two WebSocket cases (fine deltas and repeated snapshots).
8,000 answer bytes / one-byte deltas: each relevant delta line is only 137 bytes; the largest fixture line is 8,364 bytes. After delivering 7,655 answer bytes, the budget has charged 1,048,445 bytes. The next line would increase this to 1,048,582, exceeding 1,048,576. HTTP SSE and WebSocket fail at that same predicted point. With 1,024-byte chunks, the identical answer uses 42,775 cumulative data-line bytes and completes. The full fine-grained fixture would use 1,128,619 bytes.
220,000 answer bytes / 1,024-byte deltas: all answer deltas arrive, but the terminal response.completed snapshot increases cumulative accounting from 910,273 to 1,130,636 bytes and is rejected. Every individual line remains below 256 KiB. JSON with the same answer succeeds.
270,000 answer bytes: a full-content done event exceeds the distinct 262,144-byte upstream SSE-line cap. HTTP pass-through succeeds.
For the cumulative SSE failure, HTTP has already returned 200; the stream ends with an error event (status: 500, code: "server_error", message containing executor response budget exceeded 1048576 bytes) and [DONE]. It has no response.completed, response.failed, or response.incomplete terminal event. The failed responses are not stored. These are resource errors, not token-limit truncation.
Environment
Native Windows execution; Rust 1.98.1 (48a229cea), Cargo 1.98.1 (797e8a9bc), each revision's Cargo.lock, --locked.
No vLLM server, model, LiteLLM, or GPU was used in this reproduction. This isolates the gateway behavior; it is not a live-model performance or memory benchmark.
Native-build caveat: each baseline needed the same unrelated borrow correction in config.rs: after Windows replaces path separators and obtains a String, pass a borrowed string to utf8_percent_encode. No budget, parser, or storage behavior was changed in the baseline builds. The local response patch contains a Windows-only as_str() binding for this correction.
Additional context
Source cause. At 2509342, response_budget.rs still initializes a 1 MiB atomic allowance that only decreases. fetch_stream_payload charges line.len() before semantic accumulation. It counts the normalized data: prefix and serialized event data, including repeated content, rather than current retained output or queued bytes. Framing newlines and [DONE] are not charged. inference.rs separately caps upstream lines and JSON bodies. Increasing only an outbound WebSocket event cap cannot fix the cumulative failure.
Effect of #257 / 2509342. The new commit honors store: false and adds bounded connection-local checkpoints. It does not modify the cumulative response budget or upstream wire caps. A separate seven-check baseline session run reproduced the unstored fine-delta failure while coarse output, transient continuation, and durable restart controls passed. Its checkpoint ceilings (16 MiB per checkpoint, 32 MiB aggregate serialized retained checkpoints) address a different resource and should remain separate. In particular, cached/pinned/prepared checkpoint accounting should not be replaced by the per-request fix below.
Implemented and tested local approach (proposed defaults, not current upstream settings):
Resource
Proposed default
Config field in [responses]
Environment override
Logical retained response data across the turn
8 MiB
max_retained_bytes
AGENTIC_MAX_RETAINED_RESPONSE_BYTES
One upstream JSON body
16 MiB
max_upstream_json_bytes
AGENTIC_MAX_UPSTREAM_JSON_BYTES
One upstream SSE line
16 MiB
max_upstream_sse_line_bytes
AGENTIC_MAX_UPSTREAM_SSE_LINE_BYTES
One serialized client stream event
1 MiB
max_stream_event_bytes
AGENTIC_MAX_STREAM_EVENT_BYTES
Positive, validated limits; environment overrides TOML independently. No lifetime streamed-byte limit. Raising defaults alone is insufficient: retained accounting replaces the previous item-state charge as deltas/completions update that state, rather than charging all wire snapshots.
Keep the existing single normalization/ingestion path. Wire reads are bounded in inference.rs; retained output is measured in typed accumulator slots; stream delivery validates its own serialized event size; the engine owns the shared multi-round/tool/discovery allowance and persistence order.
The prototype's retained-output metric uses UTF-8 content and 32 bytes per serialized value/container, with active text/argument buffers measured by length. Strings are not rescanned on every delta. Raw gateway tool output and serialized MCP discovery retain their existing byte measurements. This is logical resource accounting, not an RSS/allocator bound; transient parsing/serialization, execution copies, and allocator capacity require separate consideration. The configurable values are policy choices to review, not benchmark-derived safe memory ceilings.
Preserve bounded delivery queues and cancellation. Retain the 1 MiB default client-event bound; reserve WebSocket routing overhead before core terminal validation so a response is not stored and then rejected only after stream_id is attached. generate: false validates both events before commit.
Use error code response_resource_limit_exceeded with the resource/limit. Non-streaming executor failures return 502. Started streams emit response.failed when the diagnostic fits, otherwise a bounded error diagnostic. Reject terminal serialization before durable persistence or checkpoint publication; keep healthy-request recovery and fix: honor WebSocket response storage with bounded sessions #257's failure/eviction semantics.
Verification of the local patch on 2509342:
52 production-binary acceptance checks passed: 14 original regression/control cases; 24 independent-limit, boundary, TOML/environment precedence, failure, recovery, and database-count checks; two tiny-event-limit diagnostic checks; 12 transient/durable session and restart checks.
Session checks include an unstored fine-delta response, same-connection continuation, a stored child of an unstored parent, full history restored after process restart, an unstored ID rejected after restart, and no cached/stored failed response. A failed same-lane continuation evicts its referenced parent; an independent request then succeeds.
Eight new Rust unit tests passed: chunk/snapshot invariance, genuinely oversized active arguments, shared retained rounds, atomic boundary/overflow behavior, UTF-8/escape/structural accounting, retained JSON metadata, defaults/partial overrides, and invalid settings. Existing focused coverage also passed for MCP/tool aggregate limits, oversized-terminal non-persistence, lifecycle validation, backpressure/disconnect, and session budget/cancellation behavior.
Full native Rust suite is not green:cargo test --locked --workspace --all-features --no-fail-fast reports the same 193 failing test cases on unpatched 2509342 and the patch, with matching names/counts: 192 fail opening Windows temporary SQLite paths; one oversized-request transport test fails with a connection-abort error. There are no additional failing test cases from the patch. The production harness uses working relative SQLite URLs and verifies persistence separately. Linux CI and live-model validation remain to be done.
The patch is a locally verified proposal, not an upstream change or a claim of complete platform validation. The regression report and the proposed default sizes can be evaluated independently.
Description
Executor-backed responses can fail after only 7,655 bytes of answer text because the request's fixed 1 MiB cumulative response budget counts serialized SSE envelopes, every delta, and repeated done/terminal snapshots. The same 8,000-byte answer succeeds when sent with coarser deltas or as non-streaming JSON. A fast, continuously draining client does not prevent the failure.
This is still reproducible at main commit
2509342dcbc12b89350cabdda17f7bb1a6b8a41b, including its new WebSocketstore: falsesession path. It affects the shared executor, not only WebSocket transport. The fixed upstream SSE-line and JSON-body ceilings are separate constraints.The resource protections added in #240 are useful, but lifetime wire-byte accounting makes ordinary response capacity depend on chunking and repeated representations. This report proposes separating configurable retained-response, upstream-wire, and client-event limits while keeping bounded queues and cancellation.
Steps to reproduce
The following deterministic upstream uses Python's standard library. It supplies an ordinary Responses lifecycle with deltas and matching full done/terminal snapshots. It requires no model, GPU, SDK, or external service.
Save the script below as
public_repro.py, then runpython public_repro.py server.In a separate terminal, build/run the gateway from the selected checkout with its locked dependencies:
cargo run --locked -p agentic-server --bin agentic-server -- --llm-api-base http://127.0.0.1:18001 --gateway-host 127.0.0.1 --gateway-port 18002 --db-url 'sqlite://budget-repro.db?mode=rwc'Run
python public_repro.py client. It requests the same 8,000-byte answer with one-byte and 1,024-byte chunks, then a 220,000-byte answer with 1,024-byte chunks.Use an isolated config/home and database; the recorded harness clears unrelated gateway/provider environment overrides. On Windows, use the native-build caveat in Environment below. The fixture bytes in this reduced script were checked against the fixture bytes used by the full HTTP/SSE/WebSocket harness.
Self-contained synthetic upstream and HTTP SSE client
Additional controls in the full harness: non-streaming JSON, HTTP pass-through with
store: false, WebSocketresponse.create, exact JSON-body boundary and boundary + 1, and a healthy request on the same WebSocket after a failed request.Expected behavior
Actual behavior
The same 14-case fixture set was run against these source revisions:
3bb55fdf55ea1a7e756cf79694b187909faf62b7, parent of #240e87eeae1a8ca09156ececbf18d04487d803247012509342dcbc12b89350cabdda17f7bb1a6b8a41b, including #2572509342The six baseline failures are four HTTP cases (fine SSE, repeated-snapshot SSE, oversized SSE line, JSON body above the fixed cap) and two WebSocket cases (fine deltas and repeated snapshots).
response.completedsnapshot increases cumulative accounting from 910,273 to 1,130,636 bytes and is rejected. Every individual line remains below 256 KiB. JSON with the same answer succeeds.upstream response exceeded 1048576 bytes). HTTP pass-through succeeds.For the cumulative SSE failure, HTTP has already returned 200; the stream ends with an
errorevent (status: 500,code: "server_error", message containingexecutor response budget exceeded 1048576 bytes) and[DONE]. It has noresponse.completed,response.failed, orresponse.incompleteterminal event. The failed responses are not stored. These are resource errors, not token-limit truncation.Environment
48a229cea), Cargo 1.98.1 (797e8a9bc), each revision'sCargo.lock,--locked.config.rs: after Windows replaces path separators and obtains aString, pass a borrowed string toutf8_percent_encode. No budget, parser, or storage behavior was changed in the baseline builds. The local response patch contains a Windows-onlyas_str()binding for this correction.Additional context
Source cause. At
2509342,response_budget.rsstill initializes a 1 MiB atomic allowance that only decreases.fetch_stream_payloadchargesline.len()before semantic accumulation. It counts the normalizeddata:prefix and serialized event data, including repeated content, rather than current retained output or queued bytes. Framing newlines and[DONE]are not charged.inference.rsseparately caps upstream lines and JSON bodies. Increasing only an outbound WebSocket event cap cannot fix the cumulative failure.Effect of #257 /
2509342. The new commit honorsstore: falseand adds bounded connection-local checkpoints. It does not modify the cumulative response budget or upstream wire caps. A separate seven-check baseline session run reproduced the unstored fine-delta failure while coarse output, transient continuation, and durable restart controls passed. Its checkpoint ceilings (16 MiB per checkpoint, 32 MiB aggregate serialized retained checkpoints) address a different resource and should remain separate. In particular, cached/pinned/prepared checkpoint accounting should not be replaced by the per-request fix below.Implemented and tested local approach (proposed defaults, not current upstream settings):
[responses]max_retained_bytesAGENTIC_MAX_RETAINED_RESPONSE_BYTESmax_upstream_json_bytesAGENTIC_MAX_UPSTREAM_JSON_BYTESmax_upstream_sse_line_bytesAGENTIC_MAX_UPSTREAM_SSE_LINE_BYTESmax_stream_event_bytesAGENTIC_MAX_STREAM_EVENT_BYTESinference.rs; retained output is measured in typed accumulator slots; stream delivery validates its own serialized event size; the engine owns the shared multi-round/tool/discovery allowance and persistence order.stream_idis attached.generate: falsevalidates both events before commit.response_resource_limit_exceededwith the resource/limit. Non-streaming executor failures return 502. Started streams emitresponse.failedwhen the diagnostic fits, otherwise a boundederrordiagnostic. Reject terminal serialization before durable persistence or checkpoint publication; keep healthy-request recovery and fix: honor WebSocket response storage with bounded sessions #257's failure/eviction semantics.Verification of the local patch on
2509342:cargo fmt --all -- --check,git diff --check, andcargo clippy --locked --workspace --all-targets --all-features -- -D warningspassed.cargo test --locked --workspace --all-features --no-fail-fastreports the same 193 failing test cases on unpatched2509342and the patch, with matching names/counts: 192 fail opening Windows temporary SQLite paths; one oversized-request transport test fails with a connection-abort error. There are no additional failing test cases from the patch. The production harness uses working relative SQLite URLs and verifies persistence separately. Linux CI and live-model validation remain to be done.The patch is a locally verified proposal, not an upstream change or a claim of complete platform validation. The regression report and the proposed default sizes can be evaluated independently.