* fix: remove bodyless 400/413 from overflow patterns to prevent false compaction
PR #67024 fixed the failover classification path (errors.ts) to not
classify bodyless 400/422 as 'format' errors. However, the compaction
trigger path (isContextOverflow in overflow.ts) still has the pattern
/^4(?:00|13)\s*(?:status code)?\s*\(no body\)/i in OVERFLOW_PATTERNS.
This means a bodyless 400 from any provider triggers compaction via
checkCompaction() -> isContextOverflow(), even though PR #67024
ensured the failover reason is null (not 'format'). The two paths are
independent: failover classification and compaction triggering use
separate functions.
A bodyless 400 cannot be a context overflow — real overflow errors
from all major providers include a descriptive body. A bare 400 with
no body is a transient/malformed error that should surface to the
user, not trigger a compaction loop.
Remove the Cerebras-specific pattern from OVERFLOW_PATTERNS. If
Cerebras returns bodyless 400/413 for actual overflow, it should be
handled with a more specific pattern or NON_OVERFLOW_PATTERNS exclusion.
AI-assisted.
* fix(ai): avoid compacting on bodyless 400 errors
---------
Co-authored-by: lykeion-dev <lykeion-dev@users.noreply.github.com>
Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
Chat Completions replay flattened every assistant text block with an empty
separator, so two distinct blocks came back as one word-joined sentence. The
same message shape survives distinctly on the Anthropic, Responses and Mistral
lanes, and the string-content flattener for strict OpenAI-compatible servers
already joins with a newline.
Two blocks arise routinely: streaming opens a new text block after a tool call,
and cross-model replay converts a thinking block into a text block.
* fix(ai): Codex stream shows internal parser text on a malformed frame
The Codex SSE parse boundary rethrew JSON.parse failures with its own
message, so the three consumers that key off the shared malformed-fragment
marker by exact string equality could not recognise it and the internal
parser wording reached the operator verbatim.
Mirror the canonical transport contract instead: a SyntaxError becomes the
shared marker with the original error kept as cause, and anything else is
rethrown untouched. The WebSocket twin is left alone because two open PRs
own that block today.
* fix(ai): preserve consumer SSE errors
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
The provider stream path interpolated the rejected SSE frame and its raw lines
into the thrown error, so text the model had generated reached logs and the
operator's error surface. The transport path already answers this exact condition
with the shared MALFORMED_STREAMING_FRAGMENT_ERROR_MESSAGE and keeps the
SyntaxError on cause, and the user-facing substitution in
formatRawAssistantErrorForUi matches that constant exactly. Align the provider
path with it so the same failure reads the same way on both paths.
The error-event branch is left as is: its payload is parsed downstream to build a
meaningful operator message, so redacting it would be a regression.
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix(ai): prevent websocket cache clobber on concurrent acquire
The WebSocket session cache acquire path (expired or non-reusable cached
entry) used unconditional delete/set around the connectWebSocket() await.
A concurrent request could install a newer entry during that await, which
the resuming acquire then clobbered, orphaning the socket that carried the
real previous_response_id continuation and corrupting multi-turn Codex
conversations.
Mirror the release path's owner-checked helpers: delete via
deleteOwnedWebSocketSession, and install via a new setOwnedWebSocketSession
that only writes when the cache still matches what this acquire left behind
(the stale entry, or undefined after removing it / on first connect). A
different cached entry means a concurrent request already won the session.
The busy-cached branch is unaffected: it returns a transient socket and
never writes the cache.
* fix(ai): close CAS loser websocket promptly without leaking its socket
* fix(ai): correct verifyClient callback type in race regression test
* fix(ai): remove unused variable and fix lint in race regression test
* test(ai): prove concurrent websocket loser closes
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(ai): tool calls fail when an unsupported schema keyword is nested
The strip walked only properties, items, anyOf, oneOf and allOf, copying every
other value through untouched. A keyword the model rejects therefore survived
inside additionalProperties, prefixItems, patternProperties, contains,
propertyNames, not, if/then/else, dependentSchemas and $defs, and the request
was refused by the provider even though the strip reported success.
Walk the same containers the caller already enumerates in
agent-tools-parameter-schema.ts.
* fix(ai): cover all nested schema containers
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(ai): avoid websocket cache clobber on stale reused release
* fix(ai): prefer-const holdFirstReuse in websocket cache race test
* fix(ai): avoid websocket cache release clobber
* fix(ai): add curly braces for websocket cache delete guard
* fix(ai): owner-check websocket cache deletes without LOC growth
* ci: bump plugin SDK public surface budget pins to match main tip
* fix(ai): satisfy curly lint for websocket cache owner delete
* ci: allowlist readonly state db and format SDK surface pins
* style: oxfmt plugin-sdk surface report
* ci: drop duplicate readonly state db kysely allowlist entry
* test(ai): prove cached websocket lease ownership
Move the contributor stale-release regression into the canonical cached transport suite and prove authenticated real WebSocket close, cancellation, and queued idle expiry cannot evict a replacement lease.
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Honor existing per-turn timeout and retry controls across OpenAI Responses, Azure Responses, and OpenAI-compatible Chat Completions. Add real SDK HTTP regressions for all three transports.\n\nRefs: #114203
Preserve trusted policy-required message delivery and native image/computer tools across Code Mode catalogs and provider transports. Keep preferred Gemini capability metadata during live discovery, escape MCP declaration keywords, and reject suspended-run resumes with missing owner identity. Add before-and-after regressions for each user path. Closes#114753.