import asyncio import queue import threading import time from collections import deque from dataclasses import dataclass, field from typing import AsyncIterator, Awaitable, Callable, Iterator from .generated.notification_registry import notification_turn_id from .generated.v2_all import ( ThreadGoalClearedNotification, ThreadGoalStatus, ThreadGoalUpdatedNotification, Turn, TurnCompletedNotification, TurnStartedNotification, TurnStatus, ) from .models import Notification, UnknownNotification class _GoalStreamClosed(Exception): """Wake a notification reader after its logical stream closes.""" def _terminal_goal_status(status: ThreadGoalStatus | None) -> bool: return status in { ThreadGoalStatus.paused, ThreadGoalStatus.blocked, ThreadGoalStatus.usage_limited, ThreadGoalStatus.budget_limited, ThreadGoalStatus.complete, } @dataclass(slots=True) class _GoalOperationState: """Private state for one goal operation exposed as a logical turn.""" thread_id: str logical_turn_id: str | None = None current_turn_id: str | None = None status: ThreadGoalStatus | None = None started_turn: Turn | None = None completed_turn: Turn | None = None interrupted: bool = False interrupt_requested: bool = False cleared: bool = False _condition: threading.Condition = field(default_factory=threading.Condition) _notifications: queue.Queue[Notification | BaseException] = field(default_factory=queue.Queue) _failure: BaseException | None = None _finished: bool = False _turn_routing_active: bool = False def observe(self, notification: Notification) -> bool: payload = notification.payload with self._condition: if not self._turn_routing_active and not isinstance( payload, ThreadGoalClearedNotification | ThreadGoalUpdatedNotification, ): return False if isinstance(payload, TurnStartedNotification): if self.logical_turn_id is None: self.logical_turn_id = payload.turn.id self.current_turn_id = payload.turn.id if self.started_turn is None: self.started_turn = payload.turn elif isinstance(payload, TurnCompletedNotification): self.completed_turn = payload.turn if self.current_turn_id == payload.turn.id: self.current_turn_id = None elif isinstance(payload, ThreadGoalUpdatedNotification): self.status = payload.goal.status if self.status == ThreadGoalStatus.active: self.cleared = False elif isinstance(payload, ThreadGoalClearedNotification): self.cleared = True if ( self.current_turn_id is None and self.completed_turn is not None and (self.cleared or _terminal_goal_status(self.status)) ): self._finished = True self._condition.notify_all() self._notifications.put(notification) return True def activate_turn_routing(self) -> None: """Accept physical turns after the previous stored goal is cleared.""" with self._condition: self._turn_routing_active = True def wait_for_start(self, timeout: float) -> str | None: """Wait for the runtime-generated first turn without consuming its event.""" deadline = time.monotonic() + timeout with self._condition: while self.started_turn is None or self.logical_turn_id is None: if self._failure is not None: raise self._failure remaining = deadline - time.monotonic() if remaining <= 0: return None self._condition.wait(remaining) return self.logical_turn_id def fail(self, exc: BaseException) -> None: with self._condition: self._failure = exc self._condition.notify_all() self._notifications.put(exc) def next_notification(self) -> Notification: item = self._notifications.get() if isinstance(item, BaseException): raise item return item def finish(self) -> None: """Mark the logical operation inactive and wake waiting controls.""" with self._condition: self._finished = True self.current_turn_id = None self._condition.notify_all() def is_finished(self) -> bool: with self._condition: return self._finished def begin_interrupt(self) -> bool: with self._condition: if self._finished: return False self.interrupt_requested = True return True def confirm_interrupt(self) -> None: with self._condition: self.interrupted = True self.interrupt_requested = False self._condition.notify_all() def cancel_interrupt(self) -> None: with self._condition: self.interrupt_requested = False self._condition.notify_all() def explicit_interrupt(self) -> bool: with self._condition: while self.interrupt_requested: self._condition.wait() return self.interrupted def active_turn(self, *, after: str | None = None) -> str | None: """Wait for the current turn, or return None once the goal has ended.""" with self._condition: while True: if self._failure is not None: raise self._failure if self._finished: return None if self.current_turn_id is not None and self.current_turn_id != after: return self.current_turn_id if self.cleared or _terminal_goal_status(self.status): return None self._condition.wait() def current_turn(self) -> str | None: """Return the current physical turn without waiting for rollover.""" with self._condition: return self.current_turn_id def resolve_active_turn(self, expected: str, active: str) -> None: """Adopt a server-reported active id when routed state is still stale.""" with self._condition: if self.current_turn_id in {None, expected}: self.current_turn_id = active self._condition.notify_all() def turn_for_interrupt(self) -> str | None: """Return an active or stale turn id that can resolve rollover races.""" with self._condition: if self.current_turn_id is not None: return self.current_turn_id if self.completed_turn is not None: return self.completed_turn.id if self.started_turn is not None: return self.started_turn.id return None def wake_notification_reader(self) -> None: """Release a reader blocked after its stream has been closed.""" self._notifications.put(_GoalStreamClosed()) def _logical_notification(notification: Notification, logical_turn_id: str) -> Notification: """Return a copy whose turn metadata uses the logical operation id.""" payload = notification.payload if isinstance(payload, UnknownNotification): params = dict(payload.params) if isinstance(params.get("turnId"), str): params["turnId"] = logical_turn_id turn = params.get("turn") if isinstance(turn, dict) and isinstance(turn.get("id"), str): params["turn"] = {**turn, "id": logical_turn_id} return Notification(notification.method, UnknownNotification(params)) turn_id = notification_turn_id(payload) if turn_id is None: return notification if hasattr(payload, "turn_id"): return Notification( notification.method, payload.model_copy(update={"turn_id": logical_turn_id}), ) if hasattr(payload, "turn"): logical_turn = payload.turn.model_copy(update={"id": logical_turn_id}) return Notification( notification.method, payload.model_copy(update={"turn": logical_turn}), ) return notification def _logical_completion( completed: TurnCompletedNotification, *, logical_turn_id: str, started: Turn | None, interrupted: bool, ) -> TurnCompletedNotification: """Coalesce the final physical completion into one logical completion.""" final_turn = completed.turn started_at = started.started_at if started is not None else final_turn.started_at duration_ms = final_turn.duration_ms if started_at is not None and final_turn.completed_at is not None: duration_ms = max(0, final_turn.completed_at - started_at) * 1000 updates: dict[str, object] = { "id": logical_turn_id, "started_at": started_at, "duration_ms": duration_ms, } if interrupted: updates["status"] = TurnStatus.interrupted return completed.model_copy(update={"turn": final_turn.model_copy(update=updates)}) @dataclass(slots=True) class _GoalStreamCursor: """Consume physical goal events as one ordered logical turn stream.""" state: _GoalOperationState started: Turn | None = None last_completed: TurnCompletedNotification | None = None failed_completion: TurnCompletedNotification | None = None status: ThreadGoalStatus | None = None active: bool = False cleared: bool = False def process(self, notification: Notification) -> tuple[list[Notification], bool]: logical_turn_id = self.state.logical_turn_id if logical_turn_id is None: raise RuntimeError("goal operation has not been bound to a logical turn id") payload = notification.payload if isinstance(payload, TurnStartedNotification): self.active = True if self.started is not None: return [], False self.started = payload.turn return [_logical_notification(notification, logical_turn_id)], False if isinstance(payload, TurnCompletedNotification): self.active = False self.last_completed = payload if payload.turn.status == TurnStatus.interrupted: return [ self._completion( notification.method, self.failed_completion or payload, ) ], True if payload.turn.status == TurnStatus.failed: self.failed_completion = payload if self.cleared or _terminal_goal_status(self.status): self.state.finish() return [self._completion(notification.method, payload)], True return [], False if self.status is None and not self.cleared: raise RuntimeError( "the connected Codex runtime did not activate goal mode for this turn" ) if self.cleared or _terminal_goal_status(self.status): self.state.finish() return [ self._completion( notification.method, self.failed_completion or payload, ) ], True return [], False events = [_logical_notification(notification, logical_turn_id)] if isinstance(payload, ThreadGoalUpdatedNotification): self.status = payload.goal.status if self.status == ThreadGoalStatus.active: self.cleared = False events = [] elif isinstance(payload, ThreadGoalClearedNotification): self.cleared = True events = [] if ( not self.active and self.last_completed is not None and (self.cleared or _terminal_goal_status(self.status)) ): self.state.finish() events.append( self._completion( "turn/completed", self.failed_completion or self.last_completed, ) ) return events, True return events, False def _completion( self, method: str, payload: TurnCompletedNotification, ) -> Notification: logical_turn_id = self.state.logical_turn_id if logical_turn_id is None: raise RuntimeError("goal operation has not been bound to a logical turn id") return Notification( method, _logical_completion( payload, logical_turn_id=logical_turn_id, started=self.started, interrupted=self.state.explicit_interrupt(), ), ) @dataclass(slots=True) class _GoalNotificationStream(Iterator[Notification]): """Closeable synchronous view of one logical goal operation.""" state: _GoalOperationState next_notification: Callable[[], Notification] unregister: Callable[[], None] cancel_goal: Callable[[], None] _cursor: _GoalStreamCursor = field(init=False) _pending: deque[Notification] = field(default_factory=deque) _closed: bool = False def __post_init__(self) -> None: self._cursor = _GoalStreamCursor(self.state) def __iter__(self) -> "_GoalNotificationStream": return self def __next__(self) -> Notification: if self._closed: raise StopIteration try: while not self._pending: notification = self.next_notification() events, completed = self._cursor.process(notification) self._pending.extend(events) if completed: self._finish() return self._pending.popleft() except _GoalStreamClosed: self.close() raise StopIteration from None except KeyboardInterrupt: self.cancel_goal() self.close() raise except BaseException: self.close() raise def _finish(self) -> None: if self._closed: return self.state.finish() self.state.wake_notification_reader() self.unregister() self._closed = True def close(self) -> None: self._finish() @dataclass(slots=True) class _AsyncGoalNotificationStream(AsyncIterator[Notification]): """Closeable asynchronous view of one logical goal operation.""" state: _GoalOperationState next_notification: Callable[[], Awaitable[Notification]] unregister: Callable[[], None] cancel_goal: Callable[[], Awaitable[None]] _cursor: _GoalStreamCursor = field(init=False) _pending: deque[Notification] = field(default_factory=deque) _closed: bool = False def __post_init__(self) -> None: self._cursor = _GoalStreamCursor(self.state) def __aiter__(self) -> "_AsyncGoalNotificationStream": return self async def __anext__(self) -> Notification: if self._closed: raise StopAsyncIteration try: while not self._pending: notification = await self.next_notification() events, completed = self._cursor.process(notification) self._pending.extend(events) if completed: self._finish() return self._pending.popleft() except _GoalStreamClosed: await self.aclose() raise StopAsyncIteration from None except asyncio.CancelledError: await self.cancel_goal() await self.aclose() raise except BaseException: await self.aclose() raise def _finish(self) -> None: if self._closed: return self.state.finish() self.state.wake_notification_reader() self.unregister() self._closed = True async def aclose(self) -> None: self._finish()