Skip to content

fix(bot-mode): deliver local DMs into a live Bot Chat owner instead of refusing - #103437

Closed
MestreY0d4-Uninter wants to merge 1 commit into
NousResearch:mainfrom
MestreY0d4-Uninter:fix/103030-bot-dm-live-owner
Closed

MestreY0d4-Uninter wants to merge 1 commit into
NousResearch:mainfrom
MestreY0d4-Uninter:fix/103030-bot-dm-live-owner

Conversation

@MestreY0d4-Uninter

Copy link
Copy Markdown

What does this PR do?

Bot Mode local DM delivery (message_agent → hermes -p <profile> chat -c "Bot Chat") is refused with target_busy whenever the target's canonical Bot Chat session is live in this gateway — even when nobody is viewing that chat and no turn is running. One background Desktop tab silently black-holes all DM/cron delivery to that teammate.

Surfaces that host live Bot Chat sessions now register a live-owner deliverer: message_agent hands the DM to the existing owner via prompt.submit(queued=True) — the composer choke point, so role alternation, persistence, and streaming behave as a typed message, and the DM runs as the NEXT turn (never interrupts a turn in flight) — instead of spawning a competing CLI that the single-owner lease fences out. With no live session for the target profile, the subprocess transport runs exactly as before.

This completes for the local delivery path the live-owner handoff that #101293 landed for the relay path (bot_relay.deliver); the single-owner fence itself is untouched.

Mechanism

  • tools/bot_mode_dm.py gains a register_live_dm_deliverer(fn) hook registry (same inversion pattern as tools/approval's _gateway_notify_cbs — tools never import the server hosting the sessions). message_agent_tool consults the hooks before spawning the subprocess; the first accepted ack returns synchronously to the sender; None falls through to the subprocess transport unchanged.
  • tui_gateway/server.py implements the deliverer: _live_bot_chat_session_for_profile(profile) (factored out of bot_relay.deliver's inline lookup so both paths agree on what "the target's Bot Chat is live here" means) + _deliver_dm_to_live_session, which submits via prompt.submit(queued=True) and returns a truthful ack. A prompt.submit error defers to the subprocess transport's error shaping (target_busy refusal + retry policy) rather than half-acknowledging.
  • Registration happens at server import (after the split-module install loop), so headless/CLI-only usage is unaffected.

Related Issue

Closes #103030 (follow-up to #100523; completes #101293 for the local path — the maintainer-endorsed "deliver through the live owner" direction)

How was this tested?

  • scripts/run_tests.sh tests/tools/ tests/test_tui_gateway_server.py tests/tui_gateway/ — 10,168 passed; the 7 failures are pre-existing/flaky on this machine and reproduce identically on a pristine main worktree (voice_mode/pulse-audio sockets, STT/compute-host timing, delegate/zombie cleanup retries, test_tui_gateway_server::test_model_options_preserves_canonical_custom_row_after_agent_init).
  • New invariant tests, proven red on main @ 79445a4 (all 6 fail there with AttributeError on the missing mechanism, then pass with the fix):
    • tests/test_tui_gateway_bot_dm_live_owner.py — registration wiring; live Bot Chat → prompt.submit(queued=True) + truthful ack; no live session → None (subprocess fallback); prompt.submit error → None (subprocess transport owns the error shaping).
    • tests/tools/test_bot_mode_dm.py::test_live_owner_delivery_skips_subprocess / ::test_live_owner_delivery_falls_back_when_no_live_session — hook preferred over subprocess (zero spawns), fallback intact when the deliverer reports no live session.
  • ruff check on touched files; scripts/check-windows-footguns.py clean.

Platforms

Linux (tested). The change is transport-layer Python with no platform-specific I/O; the refusal path is unchanged for remote/peer deliveries and for profiles with no live session.

…f refusing

The local message_agent path spawns 'hermes -p <profile> chat' for the target's
canonical Bot Chat; when this gateway hosts that chat live, the single-owner
lease fences the subprocess out and the DM is refused (target_busy) with no
turn ever run — one background tab silently black-holes all DM/cron delivery
(NousResearch#103030). Surfaces that host live sessions now register a deliverer: the tool
submits through prompt.submit(queued=True) — runs as the next turn, never
interrupts one in flight — mirroring bot_relay.deliver's relay-path handoff
(NousResearch#101293). No live session → the subprocess transport runs exactly as before.
@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/) comp/tools Tool registry, model_tools, toolsets area/sessions Session lifecycle, resume, persistence, history sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Sep 5, 2026
@alt-glitch

Copy link
Copy Markdown

This was generated by AI during triage.

Related: this competes with open #101564 for the same local Bot Mode DM delivery gap (#101060 / #100523, background-tab lease in #103030). Different mechanisms: #101564 uses a durable pending/claim/receipt handoff drained by the TUI idle poller; this PR registers a live-owner deliverer hook and submits via prompt.submit(queued=True) in the same gateway process (falls back to the subprocess transport when no live session is found). Both complement the merged relay-path fix #101293. Flagging for a maintainer to choose or compose; #102659 is the cron-side retry variant of the same family.

@teknium1

teknium1 commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Thanks @MestreY0d4-Uninter for identifying the local-versus-relay gap and proposing queued handoff to the existing Bot Chat owner. That user-facing behavior is now covered by #105442 (main 5280fe99872a58f1b9d25b00a79d240ecd035f7d), adapted from @FalconOrtiz’s #101564 owner-mailbox work. The merged path admits local DMs durably before a CLI attempt and lets the existing Desktop/TUI owner run its normal turn after active work and queued human prompts; it also works when sender and owner are separate processes, where a process-local callback registry cannot reach the owner.

Closing this alternative as superseded, with thanks for making the queued live-owner path explicit. This is coverage for an advertised live consumer, not a claim that arbitrary CLI-only owners or unrelated exact-session IPC are supported. Older running backends must restart after upgrading.

@teknium1 teknium1 closed this Sep 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/sessions Session lifecycle, resume, persistence, history comp/tools Tool registry, model_tools, toolsets comp/tui Terminal UI (ui-tui/ + tui_gateway/) P2 Medium — degraded but workaround exists sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages 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.

target_busy delivery refused by background-tab lease: ownership tracks open chat, not viewed tab (follow-up to #99956)

3 participants