Fix run cancellation and live activity stability
This commit is contained in:
+23
-4
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Any, Iterator
|
||||
from urllib import error, request
|
||||
|
||||
@@ -31,6 +32,7 @@ ANTHROPIC_MESSAGES_MODEL_PREFIXES = (
|
||||
|
||||
ANTHROPIC_VERSION = '2023-06-01'
|
||||
ANTHROPIC_MAX_TOKENS = 4096
|
||||
DEFAULT_MODEL_IDLE_TIMEOUT_SECONDS = 60.0
|
||||
|
||||
|
||||
def _join_url(base_url: str, suffix: str) -> str:
|
||||
@@ -274,6 +276,23 @@ class OpenAICompatClient:
|
||||
def __init__(self, config: ModelConfig) -> None:
|
||||
self.config = config
|
||||
|
||||
def _request_timeout_seconds(self) -> float:
|
||||
"""Return per-socket model timeout.
|
||||
|
||||
`ModelConfig.timeout_seconds` is the whole request budget used by the
|
||||
product, but urllib applies it as an idle socket timeout. Keeping it at
|
||||
one hour makes a bad upstream stream hold a session lock for an hour.
|
||||
Use a shorter idle timeout by default; any normally streaming response
|
||||
keeps extending this naturally because bytes continue to arrive.
|
||||
"""
|
||||
raw = os.environ.get('CLAW_MODEL_IDLE_TIMEOUT_SECONDS', '').strip()
|
||||
try:
|
||||
configured = float(raw) if raw else DEFAULT_MODEL_IDLE_TIMEOUT_SECONDS
|
||||
except ValueError:
|
||||
configured = DEFAULT_MODEL_IDLE_TIMEOUT_SECONDS
|
||||
configured = max(5.0, configured)
|
||||
return min(float(self.config.timeout_seconds), configured)
|
||||
|
||||
def complete(
|
||||
self,
|
||||
messages: list[dict[str, Any]],
|
||||
@@ -351,7 +370,7 @@ class OpenAICompatClient:
|
||||
method='POST',
|
||||
)
|
||||
try:
|
||||
with request.urlopen(req, timeout=self.config.timeout_seconds) as response:
|
||||
with request.urlopen(req, timeout=self._request_timeout_seconds()) as response:
|
||||
yield StreamEvent(type='message_start')
|
||||
for event_payload in self._iter_sse_payloads(response):
|
||||
yield from self._parse_stream_payload(event_payload)
|
||||
@@ -380,7 +399,7 @@ class OpenAICompatClient:
|
||||
method='POST',
|
||||
)
|
||||
try:
|
||||
with request.urlopen(req, timeout=self.config.timeout_seconds) as response:
|
||||
with request.urlopen(req, timeout=self._request_timeout_seconds()) as response:
|
||||
raw = response.read()
|
||||
except error.HTTPError as exc:
|
||||
detail = exc.read().decode('utf-8', errors='replace')
|
||||
@@ -521,7 +540,7 @@ class OpenAICompatClient:
|
||||
method='POST',
|
||||
)
|
||||
try:
|
||||
with request.urlopen(req, timeout=self.config.timeout_seconds) as response:
|
||||
with request.urlopen(req, timeout=self._request_timeout_seconds()) as response:
|
||||
raw = response.read()
|
||||
except error.HTTPError as exc:
|
||||
detail = exc.read().decode('utf-8', errors='replace')
|
||||
@@ -561,7 +580,7 @@ class OpenAICompatClient:
|
||||
method='POST',
|
||||
)
|
||||
try:
|
||||
with request.urlopen(req, timeout=self.config.timeout_seconds) as response:
|
||||
with request.urlopen(req, timeout=self._request_timeout_seconds()) as response:
|
||||
yield StreamEvent(type='message_start')
|
||||
tool_block_indexes: dict[int, int] = {}
|
||||
next_tool_index = 0
|
||||
|
||||
@@ -131,6 +131,50 @@ class RunStateStore:
|
||||
finished_at=time.time(),
|
||||
)
|
||||
|
||||
def finish_active_for_session(
|
||||
self,
|
||||
account_key: str,
|
||||
session_id: str,
|
||||
*,
|
||||
status: str,
|
||||
stage: str | None = None,
|
||||
) -> list[str]:
|
||||
now = time.time()
|
||||
with self._connect() as conn:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
select run_id, started_at
|
||||
from run_states
|
||||
where account_key = ?
|
||||
and session_id = ?
|
||||
and status in ('queued', 'running')
|
||||
""",
|
||||
(account_key, session_id),
|
||||
).fetchall()
|
||||
run_ids: list[str] = []
|
||||
for row in rows:
|
||||
run_ids.append(row['run_id'])
|
||||
started_at = row['started_at']
|
||||
elapsed_ms = (
|
||||
max(0, int((now - float(started_at)) * 1000))
|
||||
if isinstance(started_at, (int, float))
|
||||
else None
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
update run_states
|
||||
set status = ?,
|
||||
current_stage = coalesce(?, current_stage),
|
||||
cancellable = 0,
|
||||
elapsed_ms = coalesce(?, elapsed_ms),
|
||||
finished_at = ?,
|
||||
updated_at = ?
|
||||
where run_id = ?
|
||||
""",
|
||||
(status, stage, elapsed_ms, now, now, row['run_id']),
|
||||
)
|
||||
return run_ids
|
||||
|
||||
def record_event(self, run_id: str, event: dict[str, Any]) -> None:
|
||||
recorded_at = event.get('recorded_at')
|
||||
if not isinstance(recorded_at, (int, float)):
|
||||
|
||||
Reference in New Issue
Block a user