* feat(ai): continue stateful OpenAI SSE turns
Use the prior OpenAI Responses ID for native stateful HTTP/SSE turns, retry missing server state with full history in the same turn, and keep stateless/custom routes unchanged.
Co-authored-by: Markus Hartung <mail@hartmark.se>
* test(ai): assert Responses retry attempt metadata
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* refactor: consolidate coercion ownership
Centralize four canonical coercion helpers, migrate exact core and plugin duplicates through narrow Plugin SDK facades, and enforce declaration and plugin-normalization ownership boundaries.
The sweep adds eight focused SDK exports while deleting more production and tooling code than it adds. User-visible behavior is unchanged except for safer equivalent object and UI parsing at existing boundaries.
* fix: guard integer option ownership
Register resolveIntegerOption with the canonical function owner and extend the declaration-guard fixture so future local duplicates fail validation.
* fix: keep integer helpers on numeric facade
Remove the unshipped duplicate string-coerce exports and route every affected plugin consumer through the existing number-runtime contract.
* fix: point numeric coercion to number runtime
Make boundary and declaration diagnostics recommend the canonical numeric facade, with failing-before coverage for both guidance paths.
Keep provider-hosted web_search alongside Code Mode's constrained function surface when OpenAI policy authorizes it. Enforce the same authorization at Responses transport egress.
Refs #121803.
Preserve the internal null Authorization marker when resolving local no-auth provider headers, avoid classifying LM Studio prompt-template guidance as context overflow, and rebuild ai/llm-core package sources during pnpm dev/watch.
* fix(agents): keep stalled turns visible after Responses streams settle
A bare-continue turn in live QA (goal-followthrough-live, gpt-5.4 via
openai-responses) completed its SSE requests and then produced no terminal
result, delivery, timeout, or error for 6+ minutes until SIGTERM. Root cause:
turn liveness is enforced only while awaiting provider stream events (llm-idle
watchdog); queued subscription handlers are fire-and-forget during the turn and
finalize joined them unbounded and un-abortable, backed only by the 48h default
run budget. One hung delivery handler silently dead-ended the whole turn.
- Bound the pending-events join in attempt-stream-finalize with a 120s liveness
deadline plus the run-abort signal; on expiry, warn with the runId and proceed
to settlement so the run always yields a visible terminal outcome.
- Responses transports now report every SSE event via notifyLlmRequestActivity
(parity with completions/anthropic), so bookkeeping-only events keep the idle
watchdog quiet instead of counting as network silence.
* fix(agents): bound the settlement block-reply flush with the shared liveness join
ClawSweeper P1 on #120426: after the finalize-phase join times out, settlement
still awaited onBlockReplyFlush on the same wedged delivery chain (unbounded on
the supported blockReplyTimeoutMs: 0 path). Generalize the bounded join into
joinWithRunLivenessDeadline in run/abortable.ts (owner of abort/liveness
racing) and use it for both the pending-events join and the settle flush;
timeout and abort resolve with a recorded warning so the turn always reaches a
visible terminal outcome. New coverage: helper tests (hang, abort, rejection)
and a real-settle-path regression holding the flush past the deadline.
* fix(ai): preserve Responses server compaction state
Persist opaque Responses compaction items as fenced provider replay state so long stateless sessions can resume authoritative compressed history without exposing it in display or diagnostics. Carry state through worker transcripts and prune replay prefixes without splitting tool pairs.
Release note: Preserve long OpenAI Responses sessions across server-side compaction and worker restarts.
Related: #95788
* test(ai): align long-context fixtures with CI contracts
Make tool-result fixtures type-complete, use the canonical model selector helper, remove unused test-helper exports, and route the paid long-context live probe through the dedicated Gateway profile shard.
* test(ai): type mocked Responses terminal events
Give the mock SSE event collection an explicit open event shape so terminal response events coexist with output-item events under the root test typecheck.
* fix(ai): suppress rejected compaction replay
Persist a route-fenced suppression tombstone when encrypted-content recovery rejects a compaction item, so later turns do not retry the same opaque state. Preserve the tombstone through transcript redaction and cover successful fallback followed by the next turn.
* fix(ai): keep compaction suppression transport-private
Keep the suppression contract local to its sole Responses transport owner and make the regression fixture satisfy root type and lint checks without widening the Plugin SDK surface.
* refactor(ai): remove compaction suppression re-export
* fix(ai): scope compaction suppression to replay route
Keep foreign-route rejection tombstones from hiding the newest compatible Responses compaction while preserving same-route suppression.
* fix(ai): harden Responses replay recovery
Stage encrypted replay recovery so compaction is only suppressed after an attributable rejection. Preserve terminal ordering and keep provider replay within worker frame budgets without truncating opaque state.
* refactor(ai): centralize Responses output indexes
Keep normalized output identity tracking in the stream-slot owner, move response failure state to its diagnostic owner, and remove the obsolete replay clone export so exact-head static gates remain shrink-only.
* fix(ai): retain idless terminal tool identity
Use the canonical empty identity only when a provider supplies neither call nor item id, preventing terminal recovery from duplicating a done-only tool call while preserving stronger identities when available.
* fix(sessions): hide provider replay from public events
* fix(ai): stage encrypted replay recovery
* fix(ai): keep replay attempt kind internal
* fix(ai): route Azure through replay recovery
Use the shared encrypted-content retry owner for Azure Responses so compaction suppression and prompt-observer variants stay coherent across transports.
* fix(ai): harden replay persistence boundaries
Fence Azure replay by the resolved request endpoint, drop invalid replay during transcript sanitization, and surface worker-launch replay omissions through the existing redacted diagnostic path.
* fix(ai): enforce message_stop only for direct Anthropic models
The Anthropic SSE transport threw 'stream ended before message_stop' only when
a refusal buffer happened to be non-empty, so a truncated first-party stream
could pass silently while compatible proxy providers that legitimately omit
message_stop failed intermittently. The invariant now keys off the transport
contract owner: direct Anthropic models always require message_stop; proxy
providers are exempt.
* test(ai): cover proxy streams ending without message_stop
ClawSweeper P1: the proxy exemption had no focused test, so the lenient
branch could regress silently. A non-anthropic provider through a custom
endpoint now proves stopReason=stop with no error when the stream ends
without message_stop.
* 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>