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
Open
mu-rkhj: event-driven dialogue receive — acked server→daemon push, no model-visible polling#492tcovert-c137[bot] wants to merge 1 commit into
tcovert-c137[bot] wants to merge 1 commit into
Conversation
… 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes mu-rkhj: inbound dialogue for mu sessions was a model-visible
dialogue_polllong 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 idsmu:<daemon>:<session>route on the daemon segment).mu-dialogue server
dialogue_subscribe {daemon_id}registers the calling connection for push; every unacked message formu:<daemon_id>:*replays oldest-first on subscribe. Subscription replacement is always logged.say/fan_outpush each row as anotifications/dialogue.messageserver-initiated notification over the standalone SSE stream. A send is not delivery (panel finding — notifications are unacknowledged): only the daemon'sdialogue_ackmarksdelivered_at, after the message reached the session's input channel. At-least-once contract.tools/list(panel finding): discovery is the handshake'sexperimental: mu.dialoguePushcapability, 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.dialogue_polldegenerates to an immediate drain — push covers receive, waiting is meaningless. Unsubscribed peers (cc's out-of-band watcher) keep the long poll unchanged.mu daemon
on_custom_notificationparses 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).AgentInput::DialogueMessageinto 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 viaAbortOnDrop(mu-ad5x lesson).mu-core
AgentInput::DialogueMessagecarriesmessage_id+thread; the woken turn's motivation names them and instructs replying withsession_thread=<thread>so responses pair by id regardless of arrival order.Verification
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.just cigreen.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