Skip to content

feat(workspace-runtime): inbox polling for standalone molecule-mcp - #2415

Merged
HongmingWang-Rabbit merged 2 commits into
stagingfrom
feat/molecule-mcp-inbox-polling
Apr 30, 2026
Merged

HongmingWang-Rabbit merged 2 commits into
stagingfrom
feat/molecule-mcp-inbox-polling

Conversation

@HongmingWang-Rabbit

Copy link
Copy Markdown
Contributor

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-mcp was outbound-only — agents could delegate_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-destructively
  • inbox_pop(activity_id) — drop a handled message

Cursor persisted to ${CONFIGS_DIR}/.mcp_inbox_cursor; on 410 (cursor pruned) we fall back to since_secs=600 for 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 is MOLECULE_MCP_DISABLE_INBOX env, mirroring the existing MOLECULE_MCP_DISABLE_HEARTBEAT pattern.

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.app workspace 8dad3e29-c32a-4ec7-9ea7-94fe2d2d98ec:

  • _poll_once enqueued 5 a2a_receive rows from a 24h backlog, advanced cursor to newest
  • ✅ Text extraction returned actual user-typed messages ("hi can you list your peers", "check again", "what do you mean Hermes Agent...")
  • ✅ Caught a real bug: A2A SDK v1 uses kind: "text" not type: "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_message returned head non-destructively, tool_inbox_pop removed it, queue went 5→4

Test plan

  • pytest tests/test_inbox.py — 35 unit tests, all green (state transitions, cursor persist, 410 recovery, both kind + type envelope 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 tools
  • scripts/wheel_smoke.py updated to pin inbox.activate / get_state / start_poller_thread importability (matches the 0.1.16 main_sync regression-class precedent)
  • TOP_LEVEL_MODULES includes inbox so the rewriter package-prefixes imports cleanly
  • Live smoke against staging tenant (above)
  • Watch CI through to staging merge + verify wheel auto-publishes (publish-runtime.yml triggers on workspace/** + scripts/build_runtime_package.py — both touched)

🤖 Generated with Claude Code

…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>
Comment thread workspace/tests/test_inbox.py Fixed
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>
@github-actions

Copy link
Copy Markdown
Contributor

🔒 Auto-merge disabled — new commit (d061642) pushed after auto-merge was enabled. The merge queue locks SHAs at entry, so subsequent pushes can race. Verify the new commit and re-enable with gh pr merge --auto.

@HongmingWang-Rabbit
HongmingWang-Rabbit added this pull request to the merge queue Apr 30, 2026
Merged via the queue into staging with commit cc58e87 Apr 30, 2026
20 checks passed
@HongmingWang-Rabbit
HongmingWang-Rabbit deleted the feat/molecule-mcp-inbox-polling branch April 30, 2026 23:46
HongmingWang-Rabbit pushed a commit that referenced this pull request Jun 12, 2026
…mory plugin error (#2398)' (#2415) from fix/2398-enrich-commitmemory-log into main
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.

1 participant