- harness/es.py: append-only event store + проекции (образец — кооп-портал) - harness/levels.py: уровни 1с/1м/1ч/1д, контекст ∝ масштабу - harness/scheduler.py: уровневые потоки, идемпотентные тики, absorbed-стабы - harness/isolated_runner.py + _llm_worker.py: LLM-тик в сабпроцессе (глобальный asyncio-рантайм Strands не переживает 2+ create_harness) - cli.py: init/task/tasks/status/events + requeue (написан самим минутным агентом) - 9router: отдельный ключ sk-simbiot, модель auto (glm-5.3 первым)
120 lines
4.3 KiB
Python
120 lines
4.3 KiB
Python
from __future__ import annotations
|
|
|
|
import typing
|
|
import threading
|
|
from collections import deque
|
|
|
|
import anyio.to_thread
|
|
|
|
from ._exceptions import WebSocketQueueFullError
|
|
|
|
|
|
class SendQueue:
|
|
"""Bounded byte-size queue for outgoing WebSocket messages.
|
|
|
|
Messages are stored as pre-serialized strings. The queue enforces a
|
|
maximum byte budget so that unbounded buffering cannot occur during
|
|
reconnection windows.
|
|
"""
|
|
|
|
def __init__(self, max_bytes: int = 1_048_576) -> None:
|
|
self._queue: list[tuple[str, int]] = [] # (data, byte_length)
|
|
self._bytes: int = 0
|
|
self._max_bytes = max_bytes
|
|
self._lock = threading.Lock()
|
|
self._flush_done: threading.Event | None = None
|
|
|
|
def enqueue(self, data: str) -> None:
|
|
"""Append *data* to the queue.
|
|
|
|
Raises :class:`WebSocketQueueFullError` if the message would
|
|
exceed the byte-size limit.
|
|
"""
|
|
byte_length = len(data.encode("utf-8"))
|
|
with self._lock:
|
|
if self._bytes + byte_length > self._max_bytes:
|
|
raise WebSocketQueueFullError("send queue is full, message discarded")
|
|
self._queue.append((data, byte_length))
|
|
self._bytes += byte_length
|
|
|
|
def flush_sync(self, send: typing.Callable[[str], object], *, requeue_failed: bool = True) -> None:
|
|
"""Send every queued message via *send*.
|
|
|
|
If *send* raises, the failing message and all subsequent messages
|
|
are re-queued and the error is re-raised. When `requeue_failed` is
|
|
false, release the attempted message even on failure or interruption.
|
|
"""
|
|
while isinstance(pending := self._begin_flush(), threading.Event):
|
|
pending.wait()
|
|
|
|
try:
|
|
while pending:
|
|
data, byte_length = pending[0]
|
|
sent = False
|
|
try:
|
|
send(data)
|
|
sent = True
|
|
finally:
|
|
if sent or not requeue_failed:
|
|
with self._lock:
|
|
pending.popleft()
|
|
self._bytes -= byte_length
|
|
finally:
|
|
self._end_flush(pending)
|
|
|
|
async def flush_async(
|
|
self, send: typing.Callable[[str], typing.Awaitable[object]], *, requeue_failed: bool = True
|
|
) -> None:
|
|
"""Async variant of :meth:`flush_sync`."""
|
|
while isinstance(pending := self._begin_flush(), threading.Event):
|
|
# Waiting in a worker keeps the event loop responsive. Cancellation
|
|
# cannot strand ownership: the worker only waits, never acquires it.
|
|
await anyio.to_thread.run_sync(pending.wait, abandon_on_cancel=True)
|
|
|
|
try:
|
|
while pending:
|
|
data, byte_length = pending[0]
|
|
sent = False
|
|
try:
|
|
await send(data)
|
|
sent = True
|
|
finally:
|
|
if sent or not requeue_failed:
|
|
with self._lock:
|
|
pending.popleft()
|
|
self._bytes -= byte_length
|
|
finally:
|
|
self._end_flush(pending)
|
|
|
|
def _begin_flush(self) -> deque[tuple[str, int]] | threading.Event:
|
|
with self._lock:
|
|
if self._flush_done is not None:
|
|
return self._flush_done
|
|
pending = deque(self._queue)
|
|
self._queue.clear()
|
|
self._flush_done = threading.Event()
|
|
# Pending messages remain charged until their sends succeed.
|
|
return pending
|
|
|
|
def _end_flush(self, pending: deque[tuple[str, int]]) -> None:
|
|
with self._lock:
|
|
self._queue = list(pending) + self._queue
|
|
assert self._flush_done is not None
|
|
self._flush_done.set()
|
|
self._flush_done = None
|
|
|
|
def drain(self) -> list[str]:
|
|
"""Remove and return all queued messages."""
|
|
with self._lock:
|
|
items = [data for data, _ in self._queue]
|
|
self._bytes -= sum(byte_length for _, byte_length in self._queue)
|
|
self._queue.clear()
|
|
return items
|
|
|
|
def __len__(self) -> int:
|
|
with self._lock:
|
|
return len(self._queue)
|
|
|
|
def __bool__(self) -> bool:
|
|
with self._lock:
|
|
return len(self._queue) > 0
|