fix(tui): route bg process notifications to owning session, drop orphaned events - #54785
Conversation
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: LGTM
Well-scoped fix for cross-session background-process notification leakage. The ownership filter is correctly implemented in both the poller loop and the post-turn drain. The test coverage is thorough, covering orphaned events, owned events, and mixed-event scenarios.
Looks Good
- Clean implementation of ownership-aware drain
- Comprehensive test coverage (3 new tests)
- Properly drops orphaned events instead of hijacking them
- Re-queues events for live foreign sessions
- Follows existing patterns in the codebase
- Clear description of the root cause and fix
Reviewed by Hermes Agent
218f1af to
31854b7
Compare
31854b7 to
1c3e42d
Compare
Independent validation: partial fix; post-turn ordinary-completion leak remainsI independently reproduced the cross-session background-process notification leak in Hermes Desktop and validated this PR on macOS. The PR fixes the poller orphan-adoption path, but it does not currently fix the separate post-turn drain path described in the PR body. Environment
Real-world symptom (sanitized)A background task owned by Session A completed while Session B was active. Session B received: A second, unrelated Session-A process later failed and Session B also received: The persisted process metadata identified the owner as Session A, while the notifications were injected into Session B. This confirms that both successful and failed ordinary process completions can cross session boundaries. Safe minimal reproduction
terminal(
command="python -c \"import time; time.sleep(3); print('SESSION_A_DONE')\"",
background=True,
notify_on_complete=True,
)
terminal(
command="python -c \"import time; time.sleep(3); raise SystemExit(7)\"",
background=True,
notify_on_complete=True,
)Root causeThere are two independent consumers of the process-wide notification queue:
This PR adds process_registry.drain_notifications(
session_key=session.get("session_key", ""),
owns_event=lambda e: _session_owns_notification_event(sid, session, e),
)In if evt.get("type") == "async_delegation":An ordinary event with The new helper is not wired into Additional compression-lineage issueThe existing The new That means simply replacing the old call with the new helper would drop a completion that is positively owned by the current continuation after compression. Validation evidenceRED on current main with PR tests onlyThe PR's three new tests were applied to current Expected failures:
GREEN for PR-provided testsOn PR head: Related canonical regression suiteOn PR head: After applying the PR commit cleanly on current Command: Additional checks: Focused ad-hoc validation on both PR head and current-main + PRThis is ad-hoc verification of the missing path, not a claim that the complete Hermes suite failed. Recommended fixThe smallest safe fix is to generalize the existing Conceptually: evt_key = str(evt.get("session_key") or "")
origin_sid = str(evt.get("origin_ui_session_id") or "")
has_owner = bool(evt_key or origin_sid)
if owns_event is not None and has_owner and not owns_event(evt):
requeue.append(evt)
continue
if owns_event is None and session_key and evt_key and evt_key != session_key:
requeue.append(evt)
continueWhy this is preferable to direct key comparison in a new TUI-only helper:
The post-turn path should fail closed: if the current session cannot positively prove ownership, it must requeue or persist the event, never inject it into the current conversation. Regression tests still neededPlease add behavior tests through the real consumer path, not only direct helper tests:
Review verdictChanges requested / not complete yet. The poller orphan guard is useful and the existing tests pass, but the user-visible post-turn leak remains reproducible on the PR head and after applying the PR to current main. |
Per independent validation by 2751738943 on NousResearch#54785: the poller orphan-adoption fix was in place but post-turn ordinary-completion still leaked across sessions. Add ownership check to the post-turn drain path.
Supplemental commit rebased onto the current PR headFollowing up on my earlier independent validation: the PR head advanced to Commit
git cherry-pick 663080c2adeb7db18e4245bed660dd875407763cWhat the supplement adds
Verification
No competing pull request was opened. |
de00147 to
107e673
Compare
107e673 to
35803dd
Compare
Supplemental commit incorporatedCherry-picked @2751738943's supplemental commit (663080c) onto this PR branch. What changedThe supplemental commit generalizes ownership routing to all addressed notification events in
Verification438 passed, 0 failed across Credits: @2751738943 for the independent validation, root-cause analysis, and supplemental commit. |
|
Thanks for the focused ownership-routing fix. The reported defect remains on current main: The final PR commit Automated hermes-sweeper review. |
Related cluster (same cross-session notification ownership-routing family, all open): this PR (poller orphan-guard + post-turn owned-drain), #63317 (extends the ownership filter to all notification types), #42731 (post-turn only). #35667 (poller) and #57586 (compression-chain) are closed. Adding |
Clarification and differential validation of the overlapping implementations@alt-glitch, for clarity, the partial-fix finding in my earlier validation applied to the original I compared the three open implementations against the same ownership-routing contract. RecommendationAmong these overlapping PRs, #54785 at The original supplemental branch WhyThe required invariant is that an addressed notification is routed to its proven owner before any current-session-local suppression is applied. A non-owner must receive zero deliveries and must not consume or discard the owner's event.
VerificationExact PR heads (these totals confirm internal suite health, not equivalent coverage, because the branches add different tests):
Current-main simulation at
Remaining merge work
Therefore, my recommendation is: carry #54785's final routing mechanism and stronger regression suite forward as the integration baseline, rebase it onto current main while preserving the #64484 restored-event hardening and current-main tests, and do not merge #63317 or #42731 as-is. The maintainers should make the final canonical/policy decision after the remaining live-poller coverage is incorporated. |
…aned events Two complementary fixes for cross-session background-process notification leakage in the TUI/Desktop multi-session path (NousResearch#42674, NousResearch#35652). 1. Poller orphan guard: after _notification_event_belongs_elsewhere returns False, check whether the event has a non-empty session_key that differs from the current session. If so the owner session is gone — drop the event instead of hijacking it into an unrelated session transcript. 2. Post-turn drain filter: the existing drain_notifications() pops every event from the global queue regardless of ownership. Added _drain_owned_notifications() which applies the same ownership routing used by the poller (consume own, requeue foreign-live, drop orphan), and wired it into the post-turn safety drain. Complementary to PR NousResearch#42731 which addresses a separate code path in the same bug class. Together they close NousResearch#42674.
Apply positive-proof routing to every addressed notification in the registry and TUI poller while preserving ownerless legacy behavior and TUI delivery for poll-observed completions. Remove the unused exact-key drain helper and cover ordinary success and failure, origin, compression-lineage, orphan, and poll-observed paths. Complements NousResearch#54785.
Adapt the strongest NousResearch#63317 live-loop handoff regression and cover lineage lookup failure plus addressed live-loop orphans. Co-authored-by: Abhinav Bansal <abhibansal-sg@users.noreply.github.com>
35803dd to
22bdb7e
Compare
|
Rebased onto current
The force-push used an exact lease against the previously reviewed head |
Apply positive-proof routing to every addressed notification in the registry and TUI poller while preserving ownerless legacy behavior and TUI delivery for poll-observed completions. Remove the unused exact-key drain helper and cover ordinary success and failure, origin, compression-lineage, orphan, and poll-observed paths. Complements #54785.
Apply positive-proof routing to every addressed notification in the registry and TUI poller while preserving ownerless legacy behavior and TUI delivery for poll-observed completions. Remove the unused exact-key drain helper and cover ordinary success and failure, origin, compression-lineage, orphan, and poll-observed paths. Complements NousResearch#54785.
Apply positive-proof routing to every addressed notification in the registry and TUI poller while preserving ownerless legacy behavior and TUI delivery for poll-observed completions. Remove the unused exact-key drain helper and cover ordinary success and failure, origin, compression-lineage, orphan, and poll-observed paths. Complements NousResearch#54785.
What does this PR do?
Fixes cross-session background-process notification leakage in the TUI/Desktop multi-session path — two complementary paths:
1. Poller orphan guard
After
_notification_event_belongs_elsewherereturnsFalse, if the event has a non-emptysession_keythat differs from the current session, the owner is gone. Previously these orphans were consumed by whichever poller dequeued them — injecting an unrelated[IMPORTANT: Background process ...]notification into the wrong session. Now they are dropped.2. Post-turn drain ownership filter
The existing
process_registry.drain_notifications()pops every event from the global queue regardless of ownership — a turn finishing in session B could consume an event started by session A. Added_drain_owned_notifications()which applies the same ownership routing as the poller (consume own, requeue foreign-live, drop orphan), and wired it into the post-turn safety drain.Related Issues
Fixes #42674
Fixes #42731 (this PR subsumes #42731 by implementing the same
_drain_owned_notificationspost-turn drain filter, PLUS the poller orphan path fix)Related to #35652
Type of Change
Changes Made
tui_gateway/server.py_notification_poller_loopmain loop + shutdown drain (drop events whose owner session is no longer live)_drain_owned_notifications()— ownership-filtered drain that replaces the rawdrain_notifications()call in the post-turn path_run_prompt_submittests/test_tui_gateway_server.pytest_notification_poller_drops_orphaned_events— orphaned completion events are dropped, not hijackedtest_notification_poller_delivers_owned_events— regression guard: events owned by this session are still deliveredtest_drain_owned_notifications_routes_by_session_key— drain correctly routes owned/foreign/orphan/global events_notification_event_belongs_elsewheretest comment to reflect new orphan handlingHow to Test
notify_on_complete=True./newaway), then interact with session B.