Skip to content

mu-rkhj: event-driven dialogue receive — acked server→daemon push, no model-visible polling#492

Open
tcovert-c137[bot] wants to merge 1 commit into
mainfrom
cc/mu-rkhj-dialogue-push
Open

mu-rkhj: event-driven dialogue receive — acked server→daemon push, no model-visible polling#492
tcovert-c137[bot] wants to merge 1 commit into
mainfrom
cc/mu-rkhj-dialogue-push

Conversation

@tcovert-c137

@tcovert-c137 tcovert-c137 Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Fixes mu-rkhj: inbound dialogue for mu sessions was a model-visible dialogue_poll long poll — the turn froze while it blocked, queued prompts wedged behind it, and a broadcast had nothing coherent to wait for. Operator standard (on the bead): event-driven wake only; poll completes when called; replies correlate by identifier, never adjacency. Routing decision: option 3, daemon-keyed subscription (peer ids mu:<daemon>:<session> route on the daemon segment).

mu-dialogue server

  • dialogue_subscribe {daemon_id} registers the calling connection for push; every unacked message for mu:<daemon_id>:* replays oldest-first on subscribe. Subscription replacement is always logged.
  • say/fan_out push each row as a notifications/dialogue.message server-initiated notification over the standalone SSE stream. A send is not delivery (panel finding — notifications are unacknowledged): only the daemon's dialogue_ack marks delivered_at, after the message reached the session's input channel. At-least-once contract.
  • The plumbing tools are not in tools/list (panel finding): discovery is the handshake's experimental: mu.dialoguePush capability, so no model/tool surface built from the listing ever sees them. The channel's open-trust posture (say/poll already accept any identity) is documented; real authn (peer handles / capability tokens) is recorded follow-up.
  • A subscribed daemon's dialogue_poll degenerates to an immediate drain — push covers receive, waiting is meaningless. Unsubscribed peers (cc's out-of-band watcher) keep the long poll unchanged.
  • Broadcast is fire-and-return; the thread id in the tool result is the correlation handle.

mu daemon

  • Outbound MCP client: on_custom_notification parses the push and forwards it (with the connection's ack peer) to a serve-side router; import subscribes when the handshake advertises the capability, and the subscribed connection is held for the daemon's lifetime (an all-filtered import otherwise dropped it, killing the subscription — caught by the e2e).
  • Router: dedups the at-least-once window by message id (256-LRU), injects AgentInput::DialogueMessage into the addressed session, and acks only after the input landed — unknown/dead sessions stay unacked and replay on the next subscribe. Router + held connections ride the transport closure via AbortOnDrop (mu-ad5x lesson).

mu-core

  • AgentInput::DialogueMessage carries message_id + thread; the woken turn's motivation names them and instructs replying with session_thread=<thread> so responses pair by id regardless of arrival order.

Verification

  • e2e (serve_smoke::z_rkhj_dialogue_push_wakes_session_no_polling): stub dialogue server advertising only the handshake capability (zero listed tools) → daemon subscribes at import → one pushed notification wakes the session, whose motivation carries sender/content/correlation → done has no ask receipts → the daemon acks the id.
  • Server unit tests: ack-marks-exactly-once, undelivered-awaits-replay, migration idempotency, daemon-id parsing; loop test asserts the correlation handles reach the model. Full just ci green.

Gate

Three panel runs, each converging the diff: run 1 escalated with real findings (send≠delivery, unknown-session drops marked delivered, push/replay duplicate race, tool-surface exposure — all fixed above; one finding dismissed with evidence as hallucinated); run 2 narrowed to the tool-surface objection, fixed structurally; final run consensus approve in 2 rounds, all seats.

🤖 Generated with Claude Code

https://claude.ai/code/session_01REB3QZvP3arA2cLMVRgYam

… model-visible polling

Inbound dialogue for mu sessions was a model-visible dialogue_poll long
poll: the turn froze while it blocked, queued prompts wedged behind it,
and a broadcast had nothing coherent to wait for. Operator standard
(recorded on the bead): event-driven wake only; poll completes when
called; replies correlate by identifier, never adjacency.

mu-dialogue server:
- dialogue_subscribe {daemon_id}: registers the calling connection for
  push (routing = operator decision option 3, daemon-keyed subscription;
  peer ids mu:<daemon>:<session> route on the daemon segment). Replays
  every UNACKED message oldest-first on subscribe. Subscription
  replacement is always logged (open-trust channel — same posture as
  say/poll; real authn is recorded follow-up).
- say/fan_out push each inserted row to its target daemon's live
  subscription as a notifications/dialogue.message CustomNotification.
  A send is NOT delivery (panel finding — notifications are unacked
  fire-and-forget): only the daemon's dialogue_ack marks delivered_at,
  after the message reached the session's input channel. Unacked rows
  replay; the delivery contract is at-least-once.
- broadcast stays fire-and-return: the thread id in the tool result is
  the correlation handle.
- a subscribed daemon's dialogue_poll degenerates to an immediate drain
  (push covers receive). Unsubscribed peers (cc's out-of-band watcher)
  keep the long poll unchanged.

mu daemon:
- outbound MCP client: on_custom_notification parses the push and
  forwards (with the connection's ack peer) to serve's router; import
  calls dialogue_subscribe when a server exposes it (capability sniff
  on the UNfiltered surface) and holds the subscribed connection for
  the daemon's lifetime (an all-filtered import otherwise dropped it,
  killing the subscription — caught by the e2e).
- serve router: dedups the at-least-once window by message id (256-LRU),
  injects AgentInput::DialogueMessage into the addressed session, and
  acks ONLY after the input landed — unknown/dead sessions stay unacked
  and replay on the next subscribe. Router + connections ride the
  transport closure via AbortOnDrop (mu-ad5x lesson).

mu-core:
- AgentInput::DialogueMessage carries message_id + thread; the woken
  turn's motivation names them and instructs replying with
  session_thread=<thread> so responses pair by id.

e2e (serve_smoke z_rkhj): stub dialogue server → daemon subscribes at
import → push one notification → session wakes with sender/content/
correlation, done has no ask receipts, and the daemon ACKS the id.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants