Skip to content

fix(tui): autonomous background process completion notifications (#26071) - #26327

Merged
alt-glitch merged 6 commits into
mainfrom
hermes/hermes-069c1204
May 15, 2026
Merged

fix(tui): autonomous background process completion notifications (#26071)#26327
alt-glitch merged 6 commits into
mainfrom
hermes/hermes-069c1204

Conversation

@alt-glitch

@alt-glitch alt-glitch commented May 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

Background process notify_on_complete notifications now work autonomously in TUI mode, matching CLI and gateway behavior. Previously, tui_gateway/server.py never drained process_registry.completion_queue — events sat forever and the agent never learned background processes had finished.

Changes

  • tools/process_registry.py: Add format_process_notification() (moved from cli.py) and ProcessRegistry.drain_notifications() as shared helpers — eliminates duplication across CLI/gateway/TUI
  • tui_gateway/server.py: Add _notification_poller_loop daemon thread (polls completion_queue every 500ms, emits status.update + dispatches agent turn), wired into _init_session() / _finalize_session(). Post-turn drain as safety net for events arriving mid-turn.
  • cli.py: Delete _format_process_notification() (-37 lines), both drain sites now call drain_notifications() (~3 lines each)

How it works

  1. Background process finishes → _move_to_finished() puts event on completion_queue
  2. Poller thread (500ms cycle) picks it up → emits status.update (user sees activity line) → acquires session["running"] → dispatches agent turn via _run_prompt_submit
  3. If agent is busy: event is re-enqueued → post-turn drain catches it after the current turn ends
  4. If session is finalizing: poller drains remaining events before exiting

E2E verified

Both scenarios confirmed working via tmux-driven TUI:

Scenario Result
Process finishes while user is idle (no human message) ✅ Poller autonomously delivers notification, agent responds
Process finishes during active conversation ✅ Post-turn drain catches it after current turn completes

Validation

Before After
completion_queue consumers in TUI 0 poller thread + post-turn drain
Notification delivery silently lost autonomous (500ms poller)
Shared format/drain code 3 copies (CLI ×2 inline, TUI none) 1 shared helper
New tests 8 (process_registry) + 3 (poller) = 11

Closes #26071
Supersedes PR #26084 by @Ade5954 (after-turn drain only → autonomous poller)

@github-actions

github-actions Bot commented May 15, 2026

Copy link
Copy Markdown
Contributor

🔎 Lint report: hermes/hermes-069c1204 vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 8278 on HEAD, 8268 on base (🆕 +10)

🆕 New issues (57):

Rule Count
invalid-argument-type 38
unsupported-operator 12
unresolved-attribute 7
First entries
run_agent.py:2433: [invalid-argument-type] invalid-argument-type: Argument to function `query_ollama_num_ctx` is incorrect: Expected `str`, found `(str & ~AlwaysFalsy) | (dict[str, str] & ~AlwaysFalsy) | (Any & ~AlwaysFalsy) | ... omitted 5 union elements`
tests/tools/test_process_registry.py:887: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["exit code 0"]` and `str | None`
tests/run_agent/test_provider_attribution_headers.py:155: [unsupported-operator] unsupported-operator: Operator `not in` is not supported between objects of type `Literal["X-OpenRouter-Cache"]` and `Unknown | str | dict[str, str] | ... omitted 4 union elements`
tests/tools/test_process_registry.py:925: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["[IMPORTANT: Watch disabled for proc_xyz"]` and `str | None`
tests/tools/test_process_registry.py:886: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["[IMPORTANT: Background process proc_abc completed"]` and `str | None`
run_agent.py:4955: [invalid-argument-type] invalid-argument-type: Argument to function `save_trajectory` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown, Unknown] | int | dict[Unknown | str, Unknown | str | dict[str, str]]`
run_agent.py:9866: [invalid-argument-type] invalid-argument-type: Argument to function `lmstudio_model_reasoning_options` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown, Unknown] | int | dict[Unknown | str, Unknown | str | dict[str, str]]`
run_agent.py:13212: [invalid-argument-type] invalid-argument-type: Argument to function `normalize_usage` is incorrect: Expected `str | None`, found `(str & ~Literal["codex_app_server"]) | (Unknown & ~Literal["codex_app_server"]) | dict[Unknown, Unknown] | int | dict[Unknown | str, Unknown | str | dict[str, str]]`
run_agent.py:13296: [invalid-argument-type] invalid-argument-type: Argument to bound method `SessionDB.update_token_counts` is incorrect: Expected `str | None`, found `str | Unknown | dict[Unknown, Unknown] | int | dict[Unknown | str, Unknown | str | dict[str, str]]`
tests/tools/test_process_registry.py:902: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["watch pattern \"ERROR\""]` and `str | None`
run_agent.py:5601: [invalid-argument-type] invalid-argument-type: Argument to function `parse_rate_limit_headers` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown, Unknown] | int | dict[Unknown | str, Unknown | str | dict[str, str]]`
run_agent.py:2689: [invalid-argument-type] invalid-argument-type: Argument to function `build_anthropic_client` is incorrect: Expected `str`, found `(Unknown & ~AlwaysFalsy) | (str & ~AlwaysFalsy) | (dict[str, str] & ~AlwaysFalsy) | ... omitted 5 union elements`
run_agent.py:3447: [invalid-argument-type] invalid-argument-type: Argument to function `get_provider_stale_timeout` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown, Unknown] | int | dict[Unknown | str, Unknown | str | dict[str, str]]`
run_agent.py:9072: [invalid-argument-type] invalid-argument-type: Argument to function `get_provider_request_timeout` is incorrect: Expected `str | None`, found `Divergent | Divergent | str | ... omitted 4 union elements`
run_agent.py:12502: [invalid-argument-type] invalid-argument-type: Argument to function `apply_anthropic_cache_control` is incorrect: Expected `bool`, found `int | Divergent | Divergent | ... omitted 4 union elements`
run_agent.py:2740: [invalid-argument-type] invalid-argument-type: Argument to function `get_model_context_length` is incorrect: Expected `str`, found `str | dict[str, str] | Any | ... omitted 4 union elements`
run_agent.py:9072: [invalid-argument-type] invalid-argument-type: Argument to function `get_provider_request_timeout` is incorrect: Expected `str`, found `Divergent | Divergent | str | ... omitted 4 union elements`
run_agent.py:9071: [invalid-argument-type] invalid-argument-type: Argument to function `build_anthropic_client` is incorrect: Expected `str`, found `Divergent | Divergent | str | ... omitted 4 union elements`
run_agent.py:13764: [invalid-argument-type] invalid-argument-type: Argument to function `_is_oauth_token` is incorrect: Expected `str`, found `str | dict[Unknown | str, Unknown | str | dict[str, str]] | Any | ... omitted 5 union elements`
run_agent.py:10161: [unresolved-attribute] unresolved-attribute: Attribute `lower` is not defined on `dict[Unknown, Unknown] & ~AlwaysFalsy`, `int & ~AlwaysFalsy`, `dict[Unknown | str, Unknown | str | dict[str, str]] & ~AlwaysFalsy` in union `(str & ~AlwaysFalsy) | (Unknown & ~AlwaysFalsy) | (dict[Unknown, Unknown] & ~AlwaysFalsy) | ... omitted 3 union elements`
run_agent.py:7928: [invalid-argument-type] invalid-argument-type: Argument to function `get_provider_request_timeout` is incorrect: Expected `str | None`, found `str | Unknown | dict[Unknown, Unknown] | int | dict[Unknown | str, Unknown | str | dict[str, str]]`
run_agent.py:9701: [invalid-argument-type] invalid-argument-type: Argument to function `_get_anthropic_max_output` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown, Unknown] | int | dict[Unknown | str, Unknown | str | dict[str, str]]`
run_agent.py:11665: [invalid-argument-type] invalid-argument-type: Argument to function `_fixed_temperature_for_model` is incorrect: Expected `str | None`, found `str | Unknown | dict[Unknown, Unknown] | int | dict[Unknown | str, Unknown | str | dict[str, str]]`
run_agent.py:9265: [invalid-argument-type] invalid-argument-type: Argument to function `get_transport` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown, Unknown] | int | dict[Unknown | str, Unknown | str | dict[str, str]]`
run_agent.py:7482: [invalid-argument-type] invalid-argument-type: Argument to function `build_anthropic_client` is incorrect: Expected `str`, found `str | dict[Unknown | str, Unknown | str | dict[str, str]] | Any | ... omitted 5 union elements`
... and 32 more

✅ Fixed issues (45):

Rule Count
invalid-argument-type 35
unresolved-attribute 6
unsupported-operator 4
First entries
run_agent.py:13767: [invalid-argument-type] invalid-argument-type: Argument to function `len` is incorrect: Expected `Sized`, found `(str & ~AlwaysFalsy) | (dict[Unknown, Unknown] & ~AlwaysFalsy) | (Any & ~AlwaysFalsy) | ... omitted 3 union elements`
run_agent.py:7311: [invalid-argument-type] invalid-argument-type: Argument to function `_codex_cloudflare_headers` is incorrect: Expected `str`, found `Unknown | str | dict[str, str] | ... omitted 3 union elements`
run_agent.py:13230: [invalid-argument-type] invalid-argument-type: Argument to function `save_context_length` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
run_agent.py:13212: [invalid-argument-type] invalid-argument-type: Argument to function `normalize_usage` is incorrect: Expected `str | None`, found `(str & ~Literal["codex_app_server"]) | (Unknown & ~Literal["codex_app_server"]) | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
run_agent.py:9265: [invalid-argument-type] invalid-argument-type: Argument to function `get_transport` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
run_agent.py:13764: [invalid-argument-type] invalid-argument-type: Argument to function `_is_oauth_token` is incorrect: Expected `str`, found `str | dict[Unknown, Unknown] | Any | ... omitted 3 union elements`
run_agent.py:7482: [invalid-argument-type] invalid-argument-type: Argument to function `build_anthropic_client` is incorrect: Expected `str`, found `str | dict[Unknown, Unknown] | Any | ... omitted 3 union elements`
run_agent.py:13259: [invalid-argument-type] invalid-argument-type: Argument to function `estimate_usage_cost` is incorrect: Expected `str | None`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
run_agent.py:13211: [invalid-argument-type] invalid-argument-type: Argument to function `normalize_usage` is incorrect: Expected `str | None`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
run_agent.py:12502: [invalid-argument-type] invalid-argument-type: Argument to function `apply_anthropic_cache_control` is incorrect: Expected `bool`, found `int | str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | dict[Unknown, Unknown]`
run_agent.py:13257: [invalid-argument-type] invalid-argument-type: Argument to function `estimate_usage_cost` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
run_agent.py:9866: [invalid-argument-type] invalid-argument-type: Argument to function `lmstudio_model_reasoning_options` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
run_agent.py:6022: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["/"]` and `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
cli.py:9234: [invalid-argument-type] invalid-argument-type: Argument to function `estimate_usage_cost` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
run_agent.py:6022: [unresolved-attribute] unresolved-attribute: Attribute `split` is not defined on `dict[Unknown | str, Unknown | str | dict[str, str]]`, `int`, `dict[Unknown, Unknown]` in union `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
run_agent.py:14218: [invalid-argument-type] invalid-argument-type: Argument to bound method `ContextCompressor.update_model` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
tests/agent/test_codex_cloudflare_headers.py:163: [unresolved-attribute] unresolved-attribute: Attribute `startswith` is not defined on `dict[str, str]` in union `Unknown | str | dict[str, str]`
run_agent.py:9718: [invalid-argument-type] invalid-argument-type: Argument to function `get_provider_profile` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
run_agent.py:9701: [invalid-argument-type] invalid-argument-type: Argument to function `_get_anthropic_max_output` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
run_agent.py:13998: [invalid-argument-type] invalid-argument-type: Argument to function `_pool_may_recover_from_rate_limit` is incorrect: Expected `str | None`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
run_agent.py:2692: [invalid-argument-type] invalid-argument-type: Argument to function `_is_oauth_token` is incorrect: Expected `str`, found `(Unknown & ~AlwaysFalsy) | (str & ~AlwaysFalsy) | (dict[str, str] & ~AlwaysFalsy) | ... omitted 4 union elements`
run_agent.py:3447: [invalid-argument-type] invalid-argument-type: Argument to function `get_provider_stale_timeout` is incorrect: Expected `str`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
run_agent.py:11665: [invalid-argument-type] invalid-argument-type: Argument to function `_fixed_temperature_for_model` is incorrect: Expected `str | None`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
run_agent.py:13296: [invalid-argument-type] invalid-argument-type: Argument to bound method `SessionDB.update_token_counts` is incorrect: Expected `str | None`, found `str | Unknown | dict[Unknown | str, Unknown | str | dict[str, str]] | int | dict[Unknown, Unknown]`
tests/run_agent/test_provider_attribution_headers.py:156: [unsupported-operator] unsupported-operator: Operator `not in` is not supported between objects of type `Literal["X-OpenRouter-Cache-TTL"]` and `Unknown | str | dict[str, str] | ... omitted 3 union elements`
... and 20 more

Unchanged: 4271 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/tui Terminal UI (ui-tui/ + tui_gateway/) labels May 15, 2026
NishantEC

This comment was marked as outdated.

@alt-glitch
alt-glitch force-pushed the hermes/hermes-069c1204 branch from fc8dd52 to ff9a945 Compare May 15, 2026 13:46
@alt-glitch alt-glitch changed the title fix(tui): drain background process completion notifications (salvage #26084) fix(tui): autonomous background process completion notifications (#26071) May 15, 2026
@alt-glitch
alt-glitch force-pushed the hermes/hermes-069c1204 branch from ff9a945 to 6725bc7 Compare May 15, 2026 13:53
@alt-glitch
alt-glitch merged commit d541628 into main May 15, 2026
15 of 17 checks passed
@alt-glitch
alt-glitch deleted the hermes/hermes-069c1204 branch May 15, 2026 14:01
DIZ-admin pushed a commit to DIZ-admin/hermes-agent that referenced this pull request May 16, 2026
…sResearch#26071) (NousResearch#26327)

* feat(process-registry): add format_process_notification shared helper

* feat(process-registry): add drain_notifications method

* refactor(cli): use shared drain_notifications and format_process_notification

* feat(tui): add background notification poller for completion_queue

* feat(tui): wire notification poller into session init/finalize

* refactor(tui): add post-turn drain using shared helper as safety net
@Ade5954

Ade5954 commented May 18, 2026

Copy link
Copy Markdown
Contributor

#27711

Summary

Follow-up polish for #26327 (notification_poller_loop in tui_gateway/server.py) — deduplicate the event dispatch logic and fix a potential rid collision.

Changes

1. Extract _dispatch_notification() shared helper

The main poller loop and the shutdown drain loop in _notification_poller_loop() had near-identical logic (~30 lines each) for processing a completion_queue event: filter consumed events, call format_process_notification, emit status.update, acquire the running lock, and dispatch via _run_prompt_submit.

The only difference between the two copies was continue vs break when the agent is busy. This is now handled by returning a "busy" status string and letting each caller decide.

The extracted helper _dispatch_notification(evt, sid, session) -> str returns one of:

  • "dispatched" — successfully delivered as an agent turn
  • "consumed" — already consumed via wait/poll/log
  • "empty"format_process_notification returned empty
  • "busy" — agent is running; event re-enqueued
  • "dispatch_failed"_run_prompt_submit raised (logged to stderr, running lock released)

2. Fix rid collision with time.time_ns()

Replaced int(time.time() * 1000) with time.time_ns() for notification rid generation (__notif__{...}). Millisecond-resolution could produce duplicate rids when two events are dequeued in the same millisecond.

Validation

All 3 test_notification_poller_* tests pass unchanged

gweeteve pushed a commit to gweeteve/hermes-agent that referenced this pull request Jun 2, 2026
…sResearch#26071) (NousResearch#26327)

* feat(process-registry): add format_process_notification shared helper

* feat(process-registry): add drain_notifications method

* refactor(cli): use shared drain_notifications and format_process_notification

* feat(tui): add background notification poller for completion_queue

* feat(tui): wire notification poller into session init/finalize

* refactor(tui): add post-turn drain using shared helper as safety net
Seven74AI pushed a commit to Seven74AI/hermes-agent that referenced this pull request Jun 13, 2026
…sResearch#26071) (NousResearch#26327)

* feat(process-registry): add format_process_notification shared helper

* feat(process-registry): add drain_notifications method

* refactor(cli): use shared drain_notifications and format_process_notification

* feat(tui): add background notification poller for completion_queue

* feat(tui): wire notification poller into session init/finalize

* refactor(tui): add post-turn drain using shared helper as safety net
alt-glitch added a commit that referenced this pull request Jun 14, 2026
) (#26327)

* feat(process-registry): add format_process_notification shared helper

* feat(process-registry): add drain_notifications method

* refactor(cli): use shared drain_notifications and format_process_notification

* feat(tui): add background notification poller for completion_queue

* feat(tui): wire notification poller into session init/finalize

* refactor(tui): add post-turn drain using shared helper as safety net
T02200059 pushed a commit to T02200059/hermes-agent that referenced this pull request Jun 18, 2026
…sResearch#26071) (NousResearch#26327)

* feat(process-registry): add format_process_notification shared helper

* feat(process-registry): add drain_notifications method

* refactor(cli): use shared drain_notifications and format_process_notification

* feat(tui): add background notification poller for completion_queue

* feat(tui): wire notification poller into session init/finalize

* refactor(tui): add post-turn drain using shared helper as safety net
liuchanchen pushed a commit to liuchanchen/hermes-agent that referenced this pull request Jun 23, 2026
…sResearch#26071) (NousResearch#26327)

* feat(process-registry): add format_process_notification shared helper

* feat(process-registry): add drain_notifications method

* refactor(cli): use shared drain_notifications and format_process_notification

* feat(tui): add background notification poller for completion_queue

* feat(tui): wire notification poller into session init/finalize

* refactor(tui): add post-turn drain using shared helper as safety net
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
…sResearch#26071) (NousResearch#26327)

* feat(process-registry): add format_process_notification shared helper

* feat(process-registry): add drain_notifications method

* refactor(cli): use shared drain_notifications and format_process_notification

* feat(tui): add background notification poller for completion_queue

* feat(tui): wire notification poller into session init/finalize

* refactor(tui): add post-turn drain using shared helper as safety net
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/tui Terminal UI (ui-tui/ + tui_gateway/) P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

notify_on_complete notifications silently lost in TUI mode

3 participants