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>
* [AI] fix(agents): emit aborted tool results for skipped tool calls on mid-turn abort
When an abort fires mid-batch in executeToolCalls (after the assistant
message with tool_use is committed but before all tool_results are
written), the sequential and parallel dispatch loops break out and skip
the remaining tool calls. The committed assistant turn retains N tool_use
blocks but only M < N tool_results land in context.messages, leaving
orphaned tool_use that corrupts retries/continuation and triggers provider
400 errors on providers that do not synthesize missing results
(allowSyntheticToolResults=false, e.g. openai-completions/DeepSeek).
Emit aborted tool results (createErrorToolResult("Operation aborted")) for
the skipped tail in both executeToolCallsSequential and
executeToolCallsParallel so every tool_use keeps a paired tool_result.
This complements the existing write-side guard (which only covers
synthetic-enabled providers) and the persisted replay repair.
The aborted tail outcomes are routed through finalizeToolCallOutcome (via
a shared finalizeAbortedToolCall helper) so config.afterToolOutcome hooks
(audit, redaction, metadata, error-normalization) observe these skipped
calls just like every immediate or executed outcome, instead of bypassing
the outcome contract. Regression tests assert afterToolOutcome fires for
every skipped call in both dispatch modes.
Fixes#116379
Co-Authored-By: Maas <noreply@anthropic.com>
* [AI] fix(agents): emit tool_execution_start before aborted end for skipped calls
The abort-tail backfill added in #116379 emits tool_execution_end (and a
paired tool_result) for tool calls the dispatch loop never reached, but it
skipped the matching tool_execution_start. Channel/client subscribers that
pair start→end events received an end for an unknown tool-call id during
abort recovery.
Emit tool_execution_start for each skipped call before its aborted end/result,
mirroring the start event every dispatched (including immediate non-executed)
call already emits. Covers both sequential and parallel dispatch, with
regression assertions that every skipped call has a start before its end and
that start/end counts stay paired.
Co-Authored-By: Maas <noreply@anthropic.com>
* fix(agents): complete aborted tool tails safely
Fixes#116379
---------
Co-authored-by: Maas <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* 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(cron): bound restart catch-up to the active schedule
Editing a recurring job's schedule made the gateway fire it immediately on
the next restart. Startup catch-up compares the new schedule's previous slot
against lastRunAtMs, which still belongs to the retired schedule, so a slot
that never existed under the old schedule counted as missed.
Record when scheduling inputs take effect and replay a missed slot only when
it is newer than that. Jobs whose schedule never changed carry no stamp and
keep replaying every computed slot, so catch-up is unchanged for them.
The missed-slot predicate was duplicated in the runnable check and the
backoff-deferral pass; both now share one helper so the bound cannot drift.
Fixes#91944
* fix(cron): protect schedule activation ownership
---------
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>
* feat(gateway): manage audio and video attachments
* feat(gateway): preserve generated media metadata
* fix(gateway): align seeded managed media
* fix(gateway): scope pending media trust by URL
* fix(gateway): split managed media by trust
* fix(gateway): require terminal media trust unanimity
* fix(gateway): align managed media metadata types
* fix(gateway): refresh managed media contracts
* chore(ci): model managed media test seams
* test(sessions): expect canonical media kinds