Repository navigation
feat(events): multi-client session synchronization, replay, and an ACP agent surface - #14826
Merged
Merged
Conversation
This was referenced Aug 22, 2026
Closed
Closed
Closed
Contributor
✅ SSOT Configuration Compliance: Passing🎉 No hardcoded values detected that have SSOT config equivalents! |
Applied Black, isort, and autoflake to match code-quality checks. Triggered by workflow auto-fix-formatting.yml.
Applied Black, isort, and autoflake to match code-quality checks. Triggered by workflow auto-fix-formatting.yml.
Applied Black, isort, and autoflake to match code-quality checks. Triggered by workflow auto-fix-formatting.yml.
Closed
3 tasks
Owner
Author
|
CI note: Filed as #14854 with the timings. Failed jobs re-run; not treating this as a blocker on this PR's |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
4 tasks
This was referenced Aug 23, 2026
Closed
Merged
3 tasks done
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.
Research analysis of two agent protocols, plus the implementation of the multi-client gaps that
audit found.
Closes #14814, #14816, #14817, #14818, #14819, #14820, #14821, #14823, #14825.
Not closed by this PR — partially delivered, see Scope honesty below: #14822, #14824.
Tracked under umbrella #14815.
Thinking Path
The owner asked for research on the Agent Host Protocol (AHP) and the Agent Client Protocol (ACP),
then confirmed the framing that matters: AutoBot is one host with many clients — precisely the
problem AHP exists to solve — and asked for the findings implemented under a single PR.
Phase 1 established that AHP and ACP are not competitors but two layers of one stack; Microsoft's
own
guide/ahp-and-acp.mdsays so outright ("AHP is a coordination layer. ACP is a communicationlayer"), with the reference architecture speaking AHP upstream to clients and ACP downstream to
agents.
Phase 2 audited the code rather than reasoning from module names, and reframed the work:
AutoBot already had almost every ingredient — channel-scoped fan-out, monotonic per-channel
event ids, a Redis Streams replay buffer, per-session multi-connection presence — but they were
wired to three different buses, and the main chat WebSocket used the one that was structurally
single-client. Most of what follows is assembly, not invention.
The audit also surfaced a pre-existing critical defect (#14814) with a blast radius across
workflow, chain-of-thought and approval events, plus a silent data-loss path: chat history was
written from inside the WebSocket broadcast callback, so with no client attached nothing was
persisted at all.
Ordering was dependency-driven: fix delivery, make ids durable, then replay, then ownership, then
reconciliation on top of ownership, then convergence, then ACP last — because ACP's
session/loadand
session/resumeneed a durable server-side session to resume into.What Changed
Delivery and persistence (#14814)
event_manager.py— the broadcast handler was a scalar, so a second client displaced thefirst and the first disconnect cleared delivery for everyone. Now a
set, with add/discardsemantics, per-callback failure isolation, and automatic unregistration of dead sockets.
api/websockets.py— registers and unregisters this connection rather than overwriting aglobal slot;
_add_to_chat_historymoved out of the broadcast handler.register_persistence_hook), wired at startup ininitialization/lifespan.py, so history is written exactly once per publish and with zeroclients connected.
Durable sequencing and replay (#14816, #14817, #14818)
events/channel_stream.py— RedisINCRfor per-channel sequence allocation (monotonicacross restarts, safe across workers) and a per-channel Redis Stream as the replay window.
Degrades to an in-process counter when Redis is unreachable and reports replay as unavailable
rather than serving a partial history.
constants/event_stream_constants.py— env-var-backed retention/TTL constants, nohard-coded values at call sites.
events/bus.py—PersistStrategy.REDISimplemented; it previously logged critical anddropped the event.
live_event_manager.py—event_idnow comes from the durable stream, not_event_counters.api/live_events.py—subscribeacceptslast_event_id, replays what was missed, and emits anexplicit
resyncframe when it cannot produce a complete history.LiveEventService.ts— tracks a high-water mark per channel, replays from it on reconnect, andexposes
onResyncso consumers rebuild instead of appending onto a stale view.Conversation channels (#14819)
session:andchat:added to the channel grammar, with a fail-closed authorization branch(
_authorize_conversation_channel): admins bypass, the owner is allowed, unknown or unowned isdenied. Without it any authenticated caller could have subscribed to anyone's conversation.
Host-authoritative session state and reconciliation (#14820, #14821)
api/session_events.py— publishes session/chat mutations durably; wired into the create,update and delete endpoints in
api/chat_sessions.pyand intochat_history/messages.pyadd_message.useChatStore.ts—createServerSession()adopts a server-assigned id;pendingMessageIds+confirmMessage/rejectMessage/applyServerSnapshot/applyRemoteMessageimplementconfirmed/pending/optimistic with server-wins conflict resolution.
useSessionSync.ts— subscribes a conversation's channels, applies remote messages,confirms our own echoes, and rebuilds from the REST snapshot on a resync directive.
Doctrine (#14823)
docs/developer/EVENT_STATE_DOCTRINE.md— principles, design tests and anti-goals, adaptedfrom AHP's
guide/doctrine.md, plus a rules-with-teeth table tying each rule to the issue thatproduced it. Linked from the
CLAUDE.mdtrigger table.ACP agent surface (#14825)
acp/package —protocol.py(wire vocabulary),transport.py(line-delimited JSON-RPCover stdio),
server.py(dispatch, lifecycle, client callbacks),runner.py(bridge toChatWorkflowManager.process_message_stream),__main__.py(python -m acp).initialize,session/new,session/prompt,session/cancel, streamssession/update,and calls back on
session/request_permission. Permission handling fails closed: rejection,cancellation, a malformed answer and a transport error all deny.
Convergence (#14822, partial)
GlobalWebSocketService.tsmoved from/api/wsto/api/ws/live, subscribing toglobalandunwrapping channel frames into the legacy flat shape so existing listeners keep working.
PersistStrategy.NONEnow also fans out to the channel. This is behaviour-preserving, not awidening: NONE events already reached WebSocket clients through the broken global slot, so
without this the frontend migration would have silently dropped every workflow, CoT and approval
event.
action-shaped — the legacytype-shaped ping was inert againstthe channel socket.
/api/wsmarked deprecated in its docstring.Scope honesty
Two issues are not closed here, because partial delivery must not close an issue.
#14822 — repointing done, retirement outstanding.
Every in-repo reference now targets the channel socket: a new
liveEventsUrlgetter onAutoBotConfig(withwebsocketUrlmarked@deprecated),LiveEventServicereading it instead ofappending
/liveby hand,frontend_config.pyadvertising/api/ws/live, plusnetwork.tsandproject_state_manager.py. One reference was deliberately left alone: the auth allowlist entry isstartswith-matched, so/api/wsalready covers/api/ws/live— narrowing it would have silentlystarted rejecting the legacy route. Outstanding: deleting the route itself,
register_ws_broadcast, and theEventManagerbroadcast registry. That needs deployed evidencenothing external still consumes it, which a PR cannot produce.
#14824 — blocker found and cleared; the migrations remain.
The original plan ("~9 routes removed, consumers repointed") was wrong, and measuring showed why:
/ws/liveaccepted onlysubscribe,unsubscribeandping. Migrating any bidirectional routeonto it would have silently dropped half its behaviour — no way for a client to start a research
run, pause an operation, or change a metrics interval.
So this PR adds the missing primitive instead of migrating blind:
events/channel_commands.py, a{action:"command", channel, command, payload}envelope routed by channel prefix to a registeredhandler. Its notable property is that
_authorize_channelwas extracted so subscribe and commandshare one authorization rule — a second copy is how one path ends up weaker than the other — and
authorization is checked before handler lookup, since whether a handler exists is itself
information about what the deployment runs.
The nine route migrations are still to do, and remain incremental by their own acceptance criteria.
Verification
Tests added — 51 across six files, each driving a production entry point and asserting at the
far boundary rather than on a mock's call count:
event_manager_multiclient_14814_test.pyNoneregistration rejectedevents/channel_stream_replay_14818_test.pyevents/bus_channel_fanout_14822_test.pyacp/server_test.pycwdrequired; full turn streams and ends; notifications unanswered; permission denial on reject, cancel and transport errorevents/channel_commands_14824_test.pyuseChatStore.reconciliation.test.tsconfirmMessagereturns false for a foreign id; remote apply; own echo correlates instead of duplicating; snapshot replaces and clears pendingsStatic checks run locally:
python -m py_compileacross all 21 changed Python files — clean.No circular import introduced (
events/__init__pulls onlystream_managerandtypes; nothingimports back into
busorlive_event_manager).Not run locally, and why: the Python suite and the frontend type-check/lint were not executed
here. This repository's standing rules are that code is not run from the codebase and nothing is
installed into it —
autobot-frontend/node_modulesis absent and installing it is not permitted.CI is the verifier for both. Reviewers should treat CI as the gate on this PR, not my say-so,
and note that the Python suite is not a required check, so its result needs to be read
deliberately rather than inferred from an overall green.
Behavioural claims I have NOT verified on a host: that a second browser tab now receives
events, and that a reconnect replays the gap. Both are covered by unit tests at the seam, but
neither has host evidence. #14822's retirement in particular is gated on exactly that evidence.
Model Used
Claude Opus 5 (1M context).
Issues from this audit
Tracked under umbrella #14815 (all 11 registered as its sub-issues).
/api/wssingle-callback broadcastPersistStrategy.REDISdropped the eventevent_idreset on restart, diverged across workerslast_event_idsession:/chat:channel grammar + authorizationSources
· https://github.com/microsoft/agent-host-protocol
Pages relied on:
guide/doctrine.md,guide/reconciliation.md,guide/ahp-and-acp.md,specification/subscriptions.md,specification/lifecycle.md.https://agentclientprotocol.com/get-started/introduction ·
https://agentclientprotocol.com/protocol/overview ·
https://agentclientprotocol.com/protocol/session-setup ·
https://github.com/zed-industries/agent-client-protocol · registry: https://zed.dev/blog/acp-registry
Full analysis:
docs/research/agent-host-and-agent-client-protocols.md.