Skip to content

feat(agent): unified deadline layer — bounded execution primitive + timeout resolver (#85125 Phase 1) - #85147

Merged
kshitijk4poor merged 3 commits into
NousResearch:mainfrom
kshitijk4poor:feat/unified-deadline-layer
Aug 14, 2026
Merged

feat(agent): unified deadline layer — bounded execution primitive + timeout resolver (#85125 Phase 1)#85147
kshitijk4poor merged 3 commits into
NousResearch:mainfrom
kshitijk4poor:feat/unified-deadline-layer

Conversation

@kshitijk4poor

@kshitijk4poor kshitijk4poor commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

Tracking issue: #85125 (Phase 1 of 4). Draft until the plan on the tracking issue is signed off.

Phase 1 of #85125: one shared deadline layer (agent/deadline.py) so the timeout/hang bug classes get fixed once at a primitive instead of once per incident — plus the first call-site migration proving the pattern.

Changes

Validation

Check Result
tests/agent/test_deadline.py 40 passed
tests/run_agent/ executor-adjacent (-k 'concurrent or batch or timeout or executor') 100 passed, 1 skipped
tests/run_agent/test_start_order_gate.py + tests/hermes_cli/test_config.py 77 passed
E2E (isolated HERMES_HOME, real config.yaml) config-wins / env-bridge / oversized-clamp / shielded-task bound / grandchild + own-session descendant killed — all pass
Clean-import probe (model_tools + agent.tool_executor with worktree on sys.path) imports ok
ruff on changed files clean

Self-review (3-angle + Hermes-specific) applied before push: caller-cancellation task leak fixed, taskkill exit-code contract, psutil descendant sweep for own-session grandchildren, bool/NaN config rejection, kw_only BoundedResult. Known deliberate deferrals, tracked in #85125: telegram's private _await_with_thread_deadline copy migrates in Phase 2; gateway/status.py and tools/code_execution_tool.py tree-kill sites migrate in Phase 4 (both named in the module docstring).

Pure addition otherwise: no behavior change under default config, no cache impact, no message-flow changes. Later phases of #85125 migrate the sequential tool path, MCP handlers, and subprocess kill sites onto these primitives (aligning in-flight PRs #84795 / #84125 / #76822).

Part of #85125.

Summary by CodeRabbit

  • New Features

    • Added unified timeout controls for internal operations, configurable through the application configuration.
    • Added support for disabling time limits and safely clamping excessively large values.
    • Improved timeout handling for synchronous and asynchronous operations, including cleanup and clearer timeout reporting.
    • Added process-tree termination to better stop timed-out operations.
  • Documentation

    • Documented timeout configuration and precedence in the example configuration file.
  • Bug Fixes

    • Preserved compatibility with the existing environment-variable timeout setting.

…imeout resolver (NousResearch#85125 Phase 1)

One shared foundation for the timeout/hang backlog instead of per-incident
site-local fixes:

- agent/deadline.py: run_bounded_async (thread-timer deadline that survives
  a blocked event loop, generalizing the telegram adapter primitive),
  run_bounded_sync, clamp_timeout (kills the NousResearch#83220 time_t OverflowError
  class at the boundary), resolve_timeout (config.yaml timeouts: section >
  legacy env bridge > default), kill_process_tree (whole-tree termination
  for the NousResearch#71148 orphan class), DeadlineExpired (our deadline, mechanically
  distinct from provider timeouts).
- tool_executor._resolve_concurrent_tool_timeout migrates onto the resolver;
  exact legacy env-var contract preserved (default 420, 0 disables).
- timeouts: accepted as a known config root; documented in
  cli-config.yaml.example.

Pure addition otherwise — no behavior change, no new env vars, no cache
impact. Later phases (NousResearch#85125) migrate tool-execution, MCP, and subprocess
call sites onto these primitives.
- run_bounded_async: cancel + abandon the inner task when the CALLER is
  cancelled (leak the telegram original also had)
- kill_process_tree: check taskkill exit code (Windows contract parity),
  suppress console flash via windows_hide_flags, and sweep a psutil
  descendant snapshot taken before signalling — reaches grandchildren in
  their own setsid sessions and the non-group-leader case (NousResearch#71148 class)
- resolve_timeout: reject bool (YAML true would become a 1s deadline) and
  NaN config values with fall-through instead of resolving unbounded
- BoundedResult: kw_only to prevent positional transposition
- tests: real clamped-value time_t regression proof, own-session
  descendant kill, external-cancellation task cleanup, bool/NaN config
  fall-through; pin already-dead-pid contract
The os.killpg call sits below an early 'if sys.platform == win32: return'
so it can never execute on Windows; the scanner is line-based and needs
the inline marker.
@kshitijk4poor
kshitijk4poor marked this pull request as draft August 13, 2026 08:30
@alt-glitch alt-glitch added type/feature New feature or request P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard area/config Config system, migrations, profiles sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades needs-decision Awaiting maintainer decision before any implementation labels Aug 13, 2026
@kshitijk4poor
kshitijk4poor marked this pull request as ready for review August 14, 2026 19:40
@kshitijk4poor
kshitijk4poor merged commit d6a5cb9 into NousResearch:main Aug 14, 2026
48 checks passed
@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2e8e53ac-13f1-436b-9a8a-2e3f86e09732

📥 Commits

Reviewing files that changed from the base of the PR and between 90c7180 and 8b38796.

📒 Files selected for processing (5)
  • agent/deadline.py
  • agent/tool_executor.py
  • cli-config.yaml.example
  • hermes_cli/config.py
  • tests/agent/test_deadline.py

📝 Walkthrough

Walkthrough

Added a shared deadline module for timeout resolution, bounded async and sync execution, diagnostics, and process-tree termination. Registered timeout configuration, documented precedence rules, migrated concurrent tool timeout handling, and added comprehensive tests.

Changes

Unified deadline infrastructure

Layer / File(s) Summary
Deadline contracts and configuration
agent/deadline.py, hermes_cli/config.py, cli-config.yaml.example, tests/agent/test_deadline.py
Added timeout clamping, configuration/environment/default precedence, DeadlineExpired, BoundedResult, and timeout configuration documentation. Tests cover normalization, precedence, invalid values, and configuration failures.
Bounded asynchronous and synchronous execution
agent/deadline.py, tests/agent/test_deadline.py
Added daemon-timer async execution, task abandonment, blocked-loop diagnostics, cleanup callbacks, daemon-thread sync execution, timeout callbacks, and exception preservation. Tests cover completion, timeout, cancellation, cleanup, and callback behavior.
Portable process-tree termination
agent/deadline.py, tests/agent/test_deadline.py
Added Windows taskkill handling and POSIX descendant, process-group, and fallback signaling. Tests cover process-tree termination scenarios.
Concurrent tool timeout integration
agent/tool_executor.py, tests/agent/test_deadline.py
Migrated concurrent tool timeout resolution to resolve_timeout with configuration precedence, legacy environment compatibility, disabled deadlines, defaults, and invalid-value handling.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant run_bounded_async
  participant DaemonTimer
  participant asyncio.Task
  Caller->>run_bounded_async: Start awaitable with timeout
  run_bounded_async->>DaemonTimer: Schedule independent deadline
  DaemonTimer->>run_bounded_async: Signal timeout
  run_bounded_async->>asyncio.Task: Cancel or abandon task
  run_bounded_async-->>Caller: Return BoundedResult
Loading

Suggested reviewers: teknium1

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

ayushnangia added a commit to ayushnangia/hermes-agent that referenced this pull request Aug 24, 2026
…ll_process_tree

The script-timeout path used a site-local process-group kill, which
cannot reach a grandchild that created its OWN session (start_new_session
background jobs, watchdogs). Such descendants kept running after the job
reported failure (NousResearch#71148, NousResearch#59549). Migrate the timeout handler to the
unified deadline layer's kill_process_tree (NousResearch#85147, d6a5cb9): psutil
snapshots the descendant set before signalling, so own-session
grandchildren are reached too. Fallback to the site-local group kill if
the import ever fails, so the path cannot re-wedge.

The explicit script-timeout message stays the classification anchor
(NousResearch#85536's contract), keeping cron timeouts distinct from provider
timeouts.

Co-authored-by: dante32683 <dante32683@users.noreply.github.com>
Co-authored-by: supotato-ipj <supotato-ipj@users.noreply.github.com>
kshitijk4poor added a commit that referenced this pull request Aug 24, 2026
…adline.run_bounded_async (#85125 2f)

The adapter's private thread-deadline helper was the ancestor of the
unified deadline layer's run_bounded_async (#85147 was extracted from
it, plus the caller-cancellation leak fix the original still lacked).
Consolidate: the helper body becomes a thin wrapper mapping
BoundedResult.timed_out back to the asyncio.TimeoutError its 9 call
sites (the PTB retry ladder) expect. ~90 duplicated lines die, along
with the adapter-local copies of the abandon-cleanup runner and the
blocked-loop faulthandler diagnostics (both live in agent/deadline.py).

Everything the call sites rely on is preserved by the unified layer:
- thread-timer deadline that survives a blocked event loop (#63309)
- abandonment of cancellation-shielded tasks (PTB/httpcore anyio init)
- detached best-effort on_abandon cleanup (no httpx pool leak per retry)
- off-loop stack dump when the loop never processes the expiry
Plus one behavior IMPROVEMENT inherited from the shared copy: a caller
cancelling the wrapper no longer leaks the inner task unobserved (the
telegram original had that leak; the extraction fixed it).

test_telegram_init_deadline.py: the #63309 diagnostics probe now pins
the shared layer's dump hook (label "telegram-init") — same contract,
new seam. Wedge + cleanup-crash tests pass unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard needs-decision Awaiting maintainer decision before any implementation P2 Medium — degraded but workaround exists sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants