Skip to content

fix(gateway): interrupt api_server runs on shutdown timeout - #63963

Closed
necoweb3 wants to merge 1 commit into
NousResearch:mainfrom
necoweb3:fix/api-server-shutdown-interrupt
Closed

necoweb3 wants to merge 1 commit into
NousResearch:mainfrom
necoweb3:fix/api-server-shutdown-interrupt

Conversation

@necoweb3

Copy link
Copy Markdown
Contributor

Summary

This closes the shutdown-timeout follow-up gap for API-server runs.

The gateway drain now counts adapter-owned API-server work via active_agent_work_count(), but when the drain times out it still only interrupts agents stored in GatewayRunner._running_agents. /v1/runs agents live inside APIServerAdapter._active_run_agents, so they can remain running after the gateway has decided shutdown/restart must interrupt remaining work.

Why

GatewayRunner._drain_active_agents() can now time out because of active API-server work, and the timeout log explicitly reports api_server run(s). However, the interrupt path did not reach those adapter-owned agents.

That means a long-running /v1/runs task can survive the shutdown interrupt phase until process teardown, instead of receiving the same cooperative interrupt used by POST /v1/runs/{run_id}/stop.

Changes

  • Add APIServerAdapter.interrupt_active_runs(reason) to interrupt active /v1/runs agents owned by the adapter.
  • Have GatewayRunner._interrupt_running_agents() also interrupt active API-server runs.
  • Keep the post-interrupt wait loop watching API-server work as well as _running_agents.
  • Add regression coverage for both the adapter-owned interrupt hook and the gateway shutdown interrupt path.

Tests

$env:TMP='C:\tmp'; $env:TEMP='C:\tmp'; python -m pytest tests/gateway/test_api_server_active_work_drain.py -q --timeout-method=thread
18 passed in 3.17s

python -m ruff check --no-cache gateway/run.py gateway/platforms/api_server.py tests/gateway/test_api_server_active_work_drain.py

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Jul 13, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the focused shutdown follow-up. The /v1/runs premise is real on current main: gateway/run.py:5773-5829 includes adapter work in drain accounting, while gateway/run.py:5832-5840 interrupts only self._running_agents.

Problems

  • gateway/platforms/api_server.py:983-996 counts _inflight_agent_runs as active work, but the proposed hook only reaches _active_run_agents. The synchronous session-chat path calls _run_agent() without an agent_ref at gateway/platforms/api_server.py:2063-2069; _run_agent() only retains the created agent when that optional reference is supplied (gateway/platforms/api_server.py:4246-4247). Those active API turns would still reach timeout without a cooperative interrupt.

Suggested changes

  • Track interruptable agents for every _run_agent() path included in _inflight_agent_runs, and interrupt that complete adapter-owned set during shutdown.
  • Add a timeout regression for a non-/v1/runs API turn that verifies agent.interrupt() is called.

This is an automated hermes-sweeper review.

Comment thread gateway/run.py
try:
adapter = getattr(self, "adapters", {}).get(Platform.API_SERVER)
helper = getattr(adapter, "interrupt_active_runs", None)
return max(0, int(helper(reason))) if callable(helper) else 0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hook reaches only /v1/runs agents. active_agent_work_count() also includes _inflight_agent_runs, whose agents are not retained unless callers pass _run_agent(agent_ref=...); please extend tracking/interrupt coverage to those counted API routes as well.

@teknium1 teknium1 added the sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform label Jul 16, 2026
@kshitijk4poor

Copy link
Copy Markdown
Contributor

Closing as superseded by #79881, which keeps this PR's architecture and extends it to every API entry point.

@necoweb3 — you found this bug and got the design right: the duck-typed GatewayRunner._interrupt_api_server_runs() hook called from _interrupt_running_agents(), and folding _active_api_run_count() into the settle window, are both preserved in #79881 essentially as you wrote them (with credit in its description). What #79881 adds is the piece the review asked for: coverage of the non-/v1/runs paths (_inflight_agent_runs, session-chat, /v1/chat/completions) via a single registration pair in _run_agent, plus end-to-end tests binding the shutdown-interrupt behavior for those paths.

Thanks for the find and the architecture — it's shipping, just via the superset PR.

kshitijk4poor added a commit to kshitijk4poor/hermes-agent that referenced this pull request Aug 7, 2026
…window exit

Review follow-up for the salvaged NousResearch#79881/NousResearch#63963 stack: the shutdown
interrupt fires exactly once, but work can materialize AFTER that one
shot on BOTH sibling paths:

- a /v1/runs task admitted before the drain populates
  _active_run_agents only once _create_agent returns
  (queued-before-agent window);
- a _running_agents entry claimed as _AGENT_PENDING_SENTINEL is
  promoted to the real agent by track_agent() on its own schedule,
  after the one-shot walk skipped the sentinel.

Either way the settle loop waited on work nothing signaled, and the
turn went straight to the post-interrupt tool-subprocess kill — the
exact amputation the fix exists to avoid, in a rarer window.

If any work is still live when the settle loop exits, re-invoke
_interrupt_running_agents (which already skips sentinels and folds in
the API-server helper) so late-materializing agents on either path get
the cooperative interrupt. Regression test drives the real stop() path
with an accelerated loop clock and asserts exactly two interrupt
signals.
@kshitijk4poor

Copy link
Copy Markdown
Contributor

Superseded by #80917 (merging via auto-rebase), which lands this stack with per-commit authorship preserved: your commit 47132dbcd is cherry-picked verbatim at the base — the duck-typed _interrupt_api_server_runs hook, the settle-window fold, and your five tests all survive in git history under your name. You found the bug and got the architecture right; @briandevans' #79881 widened the fix from /v1/runs to all seven API agent-entry points on top of your mechanism, and a maintainer follow-up adds a settle-exit re-signal for agents that materialize after the one-shot interrupt. Thanks for the fix!

kshitijk4poor added a commit that referenced this pull request Aug 7, 2026
…window exit

Review follow-up for the salvaged #79881/#63963 stack: the shutdown
interrupt fires exactly once, but work can materialize AFTER that one
shot on BOTH sibling paths:

- a /v1/runs task admitted before the drain populates
  _active_run_agents only once _create_agent returns
  (queued-before-agent window);
- a _running_agents entry claimed as _AGENT_PENDING_SENTINEL is
  promoted to the real agent by track_agent() on its own schedule,
  after the one-shot walk skipped the sentinel.

Either way the settle loop waited on work nothing signaled, and the
turn went straight to the post-interrupt tool-subprocess kill — the
exact amputation the fix exists to avoid, in a rarer window.

If any work is still live when the settle loop exits, re-invoke
_interrupt_running_agents (which already skips sentinels and folds in
the API-server helper) so late-materializing agents on either path get
the cooperative interrupt. Regression test drives the real stop() path
with an accelerated loop clock and asserts exactly two interrupt
signals.
ma1138569845 pushed a commit to ma1138569845/dechnicAuditor-agent that referenced this pull request Aug 10, 2026
…window exit

Review follow-up for the salvaged NousResearch#79881/NousResearch#63963 stack: the shutdown
interrupt fires exactly once, but work can materialize AFTER that one
shot on BOTH sibling paths:

- a /v1/runs task admitted before the drain populates
  _active_run_agents only once _create_agent returns
  (queued-before-agent window);
- a _running_agents entry claimed as _AGENT_PENDING_SENTINEL is
  promoted to the real agent by track_agent() on its own schedule,
  after the one-shot walk skipped the sentinel.

Either way the settle loop waited on work nothing signaled, and the
turn went straight to the post-interrupt tool-subprocess kill — the
exact amputation the fix exists to avoid, in a rarer window.

If any work is still live when the settle loop exits, re-invoke
_interrupt_running_agents (which already skips sentinels and folds in
the API-server helper) so late-materializing agents on either path get
the cooperative interrupt. Regression test drives the real stop() path
with an accelerated loop clock and asserts exactly two interrupt
signals.
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…window exit

Review follow-up for the salvaged NousResearch#79881/NousResearch#63963 stack: the shutdown
interrupt fires exactly once, but work can materialize AFTER that one
shot on BOTH sibling paths:

- a /v1/runs task admitted before the drain populates
  _active_run_agents only once _create_agent returns
  (queued-before-agent window);
- a _running_agents entry claimed as _AGENT_PENDING_SENTINEL is
  promoted to the real agent by track_agent() on its own schedule,
  after the one-shot walk skipped the sentinel.

Either way the settle loop waited on work nothing signaled, and the
turn went straight to the post-interrupt tool-subprocess kill — the
exact amputation the fix exists to avoid, in a rarer window.

If any work is still live when the settle loop exits, re-invoke
_interrupt_running_agents (which already skips sentinels and folds in
the API-server helper) so late-materializing agents on either path get
the cooperative interrupt. Regression test drives the real stop() path
with an accelerated loop clock and asserts exactly two interrupt
signals.
melon-xf added a commit to melon-xf/hermes-agent that referenced this pull request Sep 3, 2026
…window exit

Review follow-up for the salvaged NousResearch#79881/NousResearch#63963 stack: the shutdown
interrupt fires exactly once, but work can materialize AFTER that one
shot on BOTH sibling paths:

- a /v1/runs task admitted before the drain populates
  _active_run_agents only once _create_agent returns
  (queued-before-agent window);
- a _running_agents entry claimed as _AGENT_PENDING_SENTINEL is
  promoted to the real agent by track_agent() on its own schedule,
  after the one-shot walk skipped the sentinel.

Either way the settle loop waited on work nothing signaled, and the
turn went straight to the post-interrupt tool-subprocess kill — the
exact amputation the fix exists to avoid, in a rarer window.

If any work is still live when the settle loop exits, re-invoke
_interrupt_running_agents (which already skips sentinels and folds in
the API-server helper) so late-materializing agents on either path get
the cooperative interrupt. Regression test drives the real stop() path
with an accelerated loop clock and asserts exactly two interrupt
signals.
smfworks pushed a commit to smfworks/hermes-agent that referenced this pull request Sep 7, 2026
…window exit

Review follow-up for the salvaged NousResearch#79881/NousResearch#63963 stack: the shutdown
interrupt fires exactly once, but work can materialize AFTER that one
shot on BOTH sibling paths:

- a /v1/runs task admitted before the drain populates
  _active_run_agents only once _create_agent returns
  (queued-before-agent window);
- a _running_agents entry claimed as _AGENT_PENDING_SENTINEL is
  promoted to the real agent by track_agent() on its own schedule,
  after the one-shot walk skipped the sentinel.

Either way the settle loop waited on work nothing signaled, and the
turn went straight to the post-interrupt tool-subprocess kill — the
exact amputation the fix exists to avoid, in a rarer window.

If any work is still live when the settle loop exits, re-invoke
_interrupt_running_agents (which already skips sentinels and folds in
the API-server helper) so late-materializing agents on either path get
the cooperative interrupt. Regression test drives the real stop() path
with an accelerated loop clock and asserts exactly two interrupt
signals.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants