`_listener_is_serving()` probes `SO_ACCEPTCONN` to decide whether a bound
socket is still accepting connections. That option is not portable: macOS
and the BSDs raise `OSError` with `ENOPROTOOPT` ("Protocol not available")
for it even on a perfectly healthy listening socket.
The surrounding `except OSError: return False` swallows that error, so the
listener is reported as degraded on every check. `_serve_forever()` then
raises `_ListenerUnavailableError("WebSocket listener did not enter a
serving state")`, and because `_is_recoverable_listener_error()` classifies
that exception as recoverable, the channel retries forever (4s -> 8s -> 16s
-> 30s backoff) and never becomes ready. On macOS this leaves the gateway
permanently at `ready: false` with the WebUI unreachable, since the
WebSocket channel is enabled by default.
Reproduced on macOS 26.5.1 / Python 3.14.3:
>>> s = socket.socket(); s.bind(("127.0.0.1", 0)); s.listen(1)
>>> s.getsockopt(socket.SOL_SOCKET, socket.SO_ACCEPTCONN)
OSError: [Errno 42] Protocol not available
Extract `_socket_is_accepting()` and treat `ENOPROTOOPT` / `EOPNOTSUPP` /
`EINVAL` as "this platform cannot answer the question", falling back to the
file-descriptor liveness check that already guarded the expression. Other
`OSError`s still propagate, and a closed socket is still rejected via the
`fileno() < 0` short circuit, so Linux behaviour is unchanged.
* refactor(agent): unify runner request fitting
* fix(agent): fit every runner model request
* fix(agent): count resumed state during request fitting
* refactor(agent): consolidate request fitting state
* refactor(agent): defer transcript assembly to runner
Keep persisted history and the fresh turn as explicit inputs until the Runner assembles the provider transcript. Preserve ContextBuilder and direct AgentRunner compatibility while making the save boundary structural.
Refs NAN-81.
* fix(providers): preserve mixed adjacent user content
* refactor(agent): decouple loop from message tool state
* refactor(agent): scope message delivery tracking per run
* refactor(agent): clarify message delivery scope name
With unifiedSession enabled, all turns are routed to the shared
unified:default session, so title generation, persistence, and change
notifications all happen on that shared session. The WebUI sidebar,
however, renders per-chat websocket:<id> sessions, so generated titles
never reach the session the user sees and every chat stays "Untitled".
Pass the origin chat_id through maybe_generate_webui_title_after_turn and
add a target_session_key to maybe_generate_webui_title so the title is
written to the per-chat session while conversation content still comes
from the shared (routed) session. When the routed session differs from
the per-chat session, the latest user/assistant exchange is used as the
title prompt since the shared session mixes content from every channel.
The shared session's title metadata is no longer read or written, so
stale shared titles can no longer leak into per-chat sessions.