feat(workspace-runtime): inbox polling for standalone molecule-mcp - #2415
Merged
Merged
Conversation
…p path
The universal MCP server (a2a_mcp_server.py) was outbound-only — agents
in standalone runtimes (Claude Code, hermes, codex, etc.) could
delegate, list peers, and write memories, but never observed the
canvas-user or peer-agent messages addressed to them. This blocked
"constantly responding" loops without forcing operators back onto a
runtime-specific channel plugin.
This PR closes the inbound gap with a poller-fed in-memory queue and
three new MCP tools:
- wait_for_message(timeout_secs?) — block until next message arrives
- inbox_peek(limit?) — list pending messages (non-destructive)
- inbox_pop(activity_id) — drop a handled message
A daemon thread polls /workspaces/:id/activity?type=a2a_receive every
5s, fills the queue from the cursor (since_id), and persists the cursor
to ${CONFIGS_DIR}/.mcp_inbox_cursor so a restart doesn't replay backlog.
On 410 (cursor pruned) we fall back to since_secs=600 for a bounded
recovery window. Activity-row → InboxMessage extraction mirrors the
molecule-mcp-claude-channel plugin's extractText (envelope shapes #1-3
+ summary fallback).
mcp_cli.main starts the poller alongside the existing register +
heartbeat threads. In-container runtimes (which have push delivery via
canvas WebSocket) skip activation, so inbox tools return an
informational "(inbox not enabled)" message instead of double-delivery.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
HongmingWang-Rabbit
requested a review
from hongmingwang-moleculeai
as a code owner
April 30, 2026 23:33
HongmingWang-Rabbit
enabled auto-merge
April 30, 2026 23:33
CodeQL flagged the bare `assert state.pop(...) is None` — under `python -O` asserts are stripped, which would skip the call entirely and the test would silently pass without exercising the code. Bind the result first so the call always runs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
|
🔒 Auto-merge disabled — new commit ( |
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.
Summary
Adds inbound A2A delivery to the universal MCP server so standalone-runtime agents (Claude Code, hermes, codex) can OBSERVE messages, not just send them.
Before:
molecule-mcpwas outbound-only — agents coulddelegate_task,list_peers,commit_memory, but never saw canvas-user or peer-agent messages addressed to them. The "constantly responding" gap from the canvas screenshots was a direct symptom.After: a daemon thread polls
/workspaces/:id/activity?type=a2a_receive&since_id=<cursor>every 5s, fills an in-memory queue, and exposes three new MCP tools:wait_for_message(timeout_secs?)— block until next message (capped 300s)inbox_peek(limit?)— list pending messages non-destructivelyinbox_pop(activity_id)— drop a handled messageCursor persisted to
${CONFIGS_DIR}/.mcp_inbox_cursor; on 410 (cursor pruned) we fall back tosince_secs=600for bounded recovery.Scope discipline
In-container runtimes (push delivery via canvas WebSocket) skip activation —
inbox.get_state()returns None and the tools surface(inbox not enabled)rather than double-deliver. Gate isMOLECULE_MCP_DISABLE_INBOXenv, mirroring the existingMOLECULE_MCP_DISABLE_HEARTBEATpattern.For Claude Code specifically: the tools work but Claude doesn't auto-poll mid-turn — the channel plugin remains the right answer for true conversational delivery there. The inbox tools are the runtime-agnostic baseline that hermes/codex agents (with continuous loops) consume natively.
Verification
Live tested against
hongmingwang.moleculesai.appworkspace8dad3e29-c32a-4ec7-9ea7-94fe2d2d98ec:_poll_onceenqueued 5 a2a_receive rows from a 24h backlog, advanced cursor to newestkind: "text"nottype: "text"for the part discriminator. My initial extractor (copied from molecule-mcp-claude-channel/server.ts:475) silently fell through to summary fallback. Now accepts both shapes — channel plugin should pick this up next sync.tool_wait_for_messagereturned head non-destructively,tool_inbox_popremoved it, queue went 5→4Test plan
pytest tests/test_inbox.py— 35 unit tests, all green (state transitions, cursor persist, 410 recovery, bothkind+typeenvelope shapes, daemon thread)pytest tests/test_platform_tools.py tests/test_a2a_mcp_server.py tests/test_mcp_cli.py— 79 tests still pass; snapshot regenerated for the 3 new toolsscripts/wheel_smoke.pyupdated to pininbox.activate / get_state / start_poller_threadimportability (matches the 0.1.16 main_sync regression-class precedent)TOP_LEVEL_MODULESincludesinboxso the rewriter package-prefixes imports cleanly🤖 Generated with Claude Code