fix(api): stop producers after run transport expires

This commit is contained in:
teknium1 2026-07-12 03:22:46 -07:00 committed by Teknium
parent 8f18fa104f
commit 837077dfae
4 changed files with 30 additions and 9 deletions

View file

@ -4304,6 +4304,11 @@ class APIServerAdapter(BasePlatformAdapter):
event_cb = self._make_run_event_callback(run_id, loop)
def _put_event_if_active(event: Optional[Dict]) -> None:
"""Enqueue only while this run still owns live transport state."""
if self._run_streams.get(run_id) is q:
q.put_nowait(event)
# Also wire stream_delta_callback so message.delta events flow through.
def _text_cb(delta: Optional[str]) -> None:
if delta is None:
@ -4311,7 +4316,7 @@ class APIServerAdapter(BasePlatformAdapter):
if run_id not in self._run_streams:
return
try:
loop.call_soon_threadsafe(q.put_nowait, {
loop.call_soon_threadsafe(_put_event_if_active, {
"event": "message.delta",
"run_id": run_id,
"timestamp": time.time(),
@ -4335,7 +4340,7 @@ class APIServerAdapter(BasePlatformAdapter):
try:
self._set_run_status(run_id, "running")
if run_id in self._stopping_run_ids:
q.put_nowait({
_put_event_if_active({
"event": "run.cancelled",
"run_id": run_id,
"timestamp": time.time(),
@ -4431,7 +4436,7 @@ class APIServerAdapter(BasePlatformAdapter):
result, usage = await asyncio.get_running_loop().run_in_executor(None, _run_sync)
if run_id in self._stopping_run_ids:
q.put_nowait({
_put_event_if_active({
"event": "run.cancelled",
"run_id": run_id,
"timestamp": time.time(),
@ -4446,7 +4451,7 @@ class APIServerAdapter(BasePlatformAdapter):
# block below never fires — issue #15561).
elif isinstance(result, dict) and result.get("failed"):
error_msg = _redact_api_error_text(result.get("error") or "agent run failed")
q.put_nowait({
_put_event_if_active({
"event": "run.failed",
"run_id": run_id,
"timestamp": time.time(),
@ -4460,7 +4465,7 @@ class APIServerAdapter(BasePlatformAdapter):
)
else:
final_response = result.get("final_response", "") if isinstance(result, dict) else ""
q.put_nowait({
_put_event_if_active({
"event": "run.completed",
"run_id": run_id,
"timestamp": time.time(),
@ -4481,7 +4486,7 @@ class APIServerAdapter(BasePlatformAdapter):
last_event="run.cancelled",
)
try:
q.put_nowait({
_put_event_if_active({
"event": "run.cancelled",
"run_id": run_id,
"timestamp": time.time(),
@ -4498,7 +4503,7 @@ class APIServerAdapter(BasePlatformAdapter):
last_event="run.failed",
)
try:
q.put_nowait({
_put_event_if_active({
"event": "run.failed",
"run_id": run_id,
"timestamp": time.time(),
@ -4520,7 +4525,7 @@ class APIServerAdapter(BasePlatformAdapter):
pass
# Sentinel: signal SSE stream to close
try:
q.put_nowait(None)
_put_event_if_active(None)
except Exception:
pass
self._active_run_agents.pop(run_id, None)

View file

@ -540,7 +540,11 @@ class TestRunLifecycleSweep:
adapter._sweep_orphaned_runs_once(time.time())
before = expired_queue.qsize()
stream_delta("must-not-buffer")
await asyncio.sleep(0)
mock_agent.interrupt("finish test")
for _ in range(40):
if run_id not in adapter._active_run_tasks:
break
await asyncio.sleep(0.05)
assert expired_queue.qsize() == before

View file

@ -276,9 +276,18 @@ Statuses are retained briefly after terminal states (`completed`, `failed`, or `
Server-Sent Events stream of the run's tool-call progress, token deltas, and lifecycle events. Designed for dashboards and thick clients that want to attach/detach without losing state.
Unconsumed event buffers expire after five minutes so a detached client cannot
grow memory indefinitely. This expires transport state only: a run that is
still executing remains visible to status polling, approval, stop control, and
concurrency accounting until its executor work actually exits. A connected SSE
subscriber continues draining normally.
### POST /v1/runs/\{run_id\}/stop
Interrupt a running agent turn. The endpoint returns immediately with `{"status": "stopping"}` while Hermes asks the active agent to stop at the next safe interruption point.
The run stays tracked as `stopping` until the executor-backed work exits, then
settles as `cancelled`; requesting stop never hides a worker that is still
running.
### POST /v1/runs/\{run_id\}/approval

View file

@ -270,9 +270,12 @@ Runs 接受简单的 `input` 字符串,以及可选的 `session_id`、`instruc
run 的工具调用进度、token 增量和生命周期事件的 Server-Sent Events 流。专为需要附加/分离而不丢失状态的仪表板和厚客户端设计。
未消费的事件缓冲区会在五分钟后过期,避免已断开的客户端导致内存无限增长。这里只会过期传输状态:仍在执行的 run 会继续保留在状态轮询、审批、停止控制和并发计数中,直到其 executor 工作真正退出。已连接的 SSE 订阅者会继续正常消费事件。
### POST /v1/runs/\{run_id\}/stop
中断正在运行的 agent 轮次。端点立即返回 `{"status": "stopping"}`,同时 Hermes 要求活跃 agent 在下一个安全中断点停止。
run 会保持 `stopping` 并继续被跟踪,直到 executor 支持的工作退出,然后进入 `cancelled`;停止请求不会隐藏仍在运行的 worker。
## Jobs API后台计划任务