Skip to main content
Glama

WebSocket Agent Hub

Author: Mann D. Shah · Financial Controller & Systems Architect
Portfolio & Related Repositories: Luna Reminder · MCP Excel Scraper · Full Portfolio

This repository lets independent AI coding sessions communicate and delegate work through one shared WebSocket hub. The working local setup supports:

  • Codex-to-Codex pairs.

  • Claude Code clients through MCP.

  • Mixed clients, provided each side is configured as a member of the same project and names the other side as its peer.

  • Multiple independent pairs on the same hub.

  • Persistent WebSocket connections with reconnect handling.

  • Task delegation, task results, and non-blocking messages.

  • Automatic Agent A/Agent B window titles.

  • An optional left/right Windows Terminal layout on a secondary monitor.

The hub transports messages; it does not run an LLM. Codex, Claude Code, or another MCP-capable client performs the actual work.

Start here — future agent instructions

Treat this README and the current source files as the source of truth. docs/SESSION_HANDOFF_2026-08-18.md describes earlier debugging and contains issues that have since been fixed.

Before changing or running the system:

  1. Confirm which project directory the user wants the agents to work in.

  2. Confirm port 8080 is either free or already serving this hub.

  3. Use unique agent IDs for every simultaneously active pair.

  4. Keep both agents in the same hub project and set each agent's AGENT_PEER_ID to the other agent.

  5. Use the same JWT_SECRET to start the hub and issue agent tokens.

  6. Never start two clients with the same agent ID. A newer connection replaces the older one.

  7. Put the complete task, relevant paths, constraints, and acceptance criteria in the delegation context; chat history is not automatically shared between models.

  8. For a clean verification, run one bounded delegation before starting long work.

Current local paths:

Purpose

Path

Hub repository

C:\Users\13072\Desktop\Python\websoket Agent communication

Supply-chain launcher

C:\Users\13072\Desktop\Python\Supply-chain-SIMulator-main\open-multi-agent-pair.bat

Cash-flow project

C:\Users\13072\Desktop\Python\project-cash-flow-risk-simulator

Related MCP server: desktop-agent-bus

How the components connect

flowchart LR
  A[Agent A<br/>Codex or Claude Code] -->|stdio MCP| MA[agent-mcp-server.mjs]
  MA -->|authenticated WebSocket| H[Agent Message Hub<br/>port 8080]
  H -->|authenticated WebSocket| MB[agent-mcp-server.mjs]
  MB -->|stdio MCP| B[Agent B<br/>Codex or Claude Code]

There are two distinct protocols:

  • agent-mcp-server.mjs speaks MCP over standard input/output to the AI client.

  • The adapter uses agent-instance.mjs to speak the hub's authenticated protocol over ws://127.0.0.1:8080/ws.

Do not configure Codex or Claude Code to treat the hub's /ws URL as a remote MCP server. It is not an MCP endpoint.

One-file launcher for any project

Copy the thin wrapper into a project folder. You may rename the copy:

$hub = "C:\Users\13072\Desktop\Python\websoket Agent communication\open-multi-agent-pair.bat"
$project = "C:\path\to\your-project\open-agent-pair.bat"
Copy-Item -LiteralPath $hub -Destination $project

That .bat is the only Agent Hub file needed inside the project. It delegates to the canonical central launcher, so copied project wrappers do not become stale when the hub implementation changes. Check prerequisites without starting anything:

& "C:\path\to\your-project\open-agent-pair.bat" --diagnose

Then double-click it and enter a pair name, or provide one directly:

& "C:\path\to\your-project\open-agent-pair.bat" inventory_review

To open a separate hub console with live logs for a newly started pair:

& "C:\path\to\your-project\open-agent-pair.bat" --show-hub inventory_review

The visible hub console closes automatically after the final agent disconnects. If an authenticated hub is already running, the launcher safely reuses it but cannot attach its existing output to a new console.

The launcher:

  1. Derives a stable project ID from the project folder name and a hash of its absolute path.

  2. Reuses port 8080 only after both health and administrative-authentication probes pass.

  3. Otherwise starts the loopback-only in-memory hub, hidden by default or visible with --show-hub.

  4. Idempotently creates the project, agents, memberships, and fresh one-hour JWTs.

  5. Opens two isolated Codex terminals and waits until both MCP agents are online.

  6. Cleans up terminals and a launcher-owned hub if any startup stage fails.

  7. Stops an unused hub after 60 seconds, or an active hub five seconds after its last agent disconnects.

The wrapper does not start, stop, or modify the project's own application processes. Runtime credentials remain in the central hub directory, not in individual projects.

Always use a new pair name for another simultaneously active pair.

Starting task prompt for Agent A

Paste a real task into Agent A. For example:

Review this supply-chain simulator for correctness and maintainability. First map the architecture yourself. Delegate an independent audit of the simulation calculations and edge cases to external Agent B using agent_hub.delegate_task. Preserve the returned routing IDs, use send_update for any mid-task correction, and use wait_for_task_result for the late result. Verify Agent B's evidence, then give me one combined ranked report. Do not edit files.

Agent B should already be calling wait_for_task. After receiving a task, it must do the work in the same turn, call check_for_updates at safe checkpoints approximately every 30 seconds, apply any returned update, and call send_task_result. If cancellation is returned, it must stop starting new work and call confirm_task_cancelled, then wait again.

Agent roles and MCP tools

The MCP adapter exposes twelve tools:

Tool

Intended use

delegate_task

Send a bounded task and receive routing IDs; use wait_for_task_result for a late result.

wait_for_task_result

Resume waiting for a previously delegated result.

cancel_task

Request authoritative cooperative cancellation of an active task.

wait_for_task_cancellation

Resume waiting for cancellation to be surfaced or confirmed.

wait_for_task

Wait for delegated work, an update/cancellation, or a peer's send_message.

send_task_result

Return completed, blocked, or failed work using the exact routing IDs.

send_update

Send a correction or added instruction for an active task.

wait_for_update_read

Resume waiting for the peer to read an update.

check_for_updates

Surface unread updates or cancellation at a safe worker checkpoint.

confirm_task_cancelled

Confirm that the worker stopped starting new work.

send_message

Send a non-blocking informational message to the peer.

wait_for_message

Wait for a non-task informational message.

Non-task messages after a task finishes

send_message/wait_for_message is a separate channel from task delegation, and a completed task's correlationId cannot carry follow-ups: the hub closes that conversation on send_task_result, and any later send_update against it is rejected with CONVERSATION_CLOSED. Use send_message for anything sent after a task is done.

A peer message is now delivered two ways so neither side has to remember to poll for it separately:

  • A worker sitting in wait_for_task is interrupted immediately by a peer's send_message and gets back status: "message_received". Handle it and call wait_for_task again right away to resume listening for both tasks and further messages.

  • Any other queued peer message is attached as a pendingPeerMessages array on the JSON result of the next tool call either side makes, whatever that tool is. This is how a coordinator — which has no blocking idle loop once it returns control to the user — picks up a message sent after its last task result: it surfaces on the very next delegate_task, wait_for_task_result, send_update, check_for_updates, or any other call.

A coordinator that expects a reply and is about to end its turn has no way to keep listening after that point; it will only see the reply once it calls a tool again (e.g. on the user's next request, or a follow-up delegation). This is a hard limit of MCP's pull-only transport, not something a prompt or a code change on either side can work around: nothing can push text into an idle client's context, and Codex has no hook mechanism to run code before a fresh prompt the way Claude Code does.

Two things are handled outside the model entirely, in agent-mcp-server.mjs, so they do not depend on either agent remembering anything:

  • Durability. Every incoming peer message is written to a small per-agent mailbox file (AGENT_HUB_MAILBOX_DIR, one JSON file per message) the instant it arrives, before it ever reaches the model. wait_for_task, wait_for_message, and the pendingPeerMessages drain all delete a message's file the moment they hand it to the model, and a fresh MCP process re-loads any leftover files on startup. A message an agent never got around to reading survives that agent's process restarting or crashing; it does not survive being read.

  • Out-of-band notice. The instant a peer message arrives, agent-mcp-server.mjs writes it to stderr and (on Windows, unless AGENT_HUB_NOTIFY=false) pops a toast notification, independent of whether either agent's model is doing anything at all. This is the only channel that reliably reaches you the moment a message lands rather than only on the peer's next tool call; use it to decide whether to prompt the idle agent yourself.

Coordinator behavior:

  1. Work on the main task.

  2. Call delegate_task only for a useful bounded subtask, parallel investigation, or independent review.

  3. Preserve its correlationId and requestMessageId; use send_update for mid-task corrections and wait_for_task_result for the result.

  4. Use cancel_task when the user cancels, then wait_for_task_cancellation if the peer has not surfaced it yet.

  5. Supply enough context for the other model to work without the coordinator's chat history.

  6. Wait for and verify the returned evidence.

  7. Incorporate the result into the final answer.

  8. Before ending the turn, check whether the last tool result carried a pendingPeerMessages field; if so, read those messages, since they are the only opportunity to see a worker's post-task send_message before the turn ends.

  9. At the start of each new turn, before anything else, call wait_for_message with timeoutSeconds: 2 as routine housekeeping to silently pick up anything the worker sent while idle. A timeout result there is normal and means nothing arrived; do not mention it to the user unless it returns an actual message.

Worker behavior:

  1. Call wait_for_task({"timeoutSeconds":900}).

  2. If it returns task_received, continue in the same turn and execute the task immediately.

  3. During the task, call check_for_updates at safe checkpoints approximately every 30 seconds: before mutations, between bounded batches, after long-running tools, and before sending the result. This is a target, not a forced interruption interval.

  4. If it returns an update, apply it before continuing. If it returns task_cancellation_requested, stop starting new work and call confirm_task_cancelled with the returned cancelMessageId.

  5. Call send_task_result with the exact requestMessageId, correlationId, and toAgentId returned by wait_for_task. If it returns task_result_deferred, apply the update and retry with the same routing IDs.

  6. Call wait_for_task again after the task is complete or cancelled.

  7. If a wait returns timeout, simply call it again when continued; the WebSocket connection itself is not limited to 900 seconds.

  8. If wait_for_task returns message_received instead of a task, that is a non-task message from the coordinator, not work; handle it and call wait_for_task again immediately. Also check any tool result for a pendingPeerMessages field before moving on.

Connecting Claude Code

The supported Claude client is Claude Code with a local stdio MCP server. Claude Code and Codex can use the same agent-mcp-server.mjs because MCP is the client-facing boundary.

1. Start the hub

If it is not already running:

Set-Location -LiteralPath "C:\Users\13072\Desktop\Python\websoket Agent communication"
$env:STORE_MODE = "in-memory"
$env:SEED_DEMO = "true"
$env:PORT = "8080"
$env:HOST = "127.0.0.1"
$env:JWT_SECRET = "local-test-secret-change-this"
$env:ADMIN_TOKEN = "development-admin-token"
npm start

Check it from another terminal:

Invoke-RestMethod http://127.0.0.1:8080/health

Expected result:

{"status":"ok"}

2. Provision the Claude identities

Every Claude session needs:

  • A unique agent ID already registered with the hub.

  • Membership in the same project as its peer.

  • A JWT whose subject is that exact agent ID.

  • The other agent's exact ID in AGENT_PEER_ID.

The tested idempotent provisioning implementation is the Provisioning ... PowerShell block in open-multi-agent-pair.bat. Reuse that block and change the project ID, project name, IDs, names, and roles. The required REST operations are:

POST /projects
POST /agents                    (once for each identity)
POST /projects/:projectId/agents (once for each membership)

Administrative requests require:

x-admin-token: development-admin-token

For local testing, issue two tokens after the TypeScript hub has been built:

$env:JWT_SECRET = "local-test-secret-change-this"
node "C:\Users\13072\Desktop\Python\websoket Agent communication\create-demo-agent-tokens.mjs" `
  "claude_pair1_coordinator" `
  "claude_pair1_worker"

The output contains AGENT_CODEX_A_TOKEN=... and AGENT_CODEX_B_TOKEN=.... The variable names are historical; the JWTs work with any compatible client.

Never commit these tokens or the local development secrets.

3. Add a project-scoped Claude MCP configuration

Create .mcp.json in the project Claude will work on:

{
  "mcpServers": {
    "agent_hub": {
      "type": "stdio",
      "command": "node",
      "args": [
        "C:/Users/13072/Desktop/Python/websoket Agent communication/agent-mcp-server.mjs"
      ],
      "env": {
        "AGENT_HUB_URL": "${AGENT_HUB_URL:-ws://127.0.0.1:8080/ws}",
        "AGENT_TOKEN": "${AGENT_TOKEN}",
        "AGENT_ID": "${AGENT_ID}",
        "AGENT_NAME": "${AGENT_NAME}",
        "AGENT_TYPE": "${AGENT_TYPE:-claude}",
        "AGENT_PROJECT_ID": "${AGENT_PROJECT_ID}",
        "AGENT_PEER_ID": "${AGENT_PEER_ID}",
        "AGENT_MCP_REQUEST_TIMEOUT_MS": "900000",
        "AGENT_RECONNECT_MAX_MS": "30000"
      },
      "timeout": 920000
    }
  }
}

Claude Code asks for approval before using a project-scoped MCP server from .mcp.json. Review and approve this local server when prompted. Check its status with:

claude mcp list

You can also use /mcp inside Claude Code.

4. Open Claude Agent A

In the target project directory, set the coordinator's values and launch Claude:

$env:AGENT_HUB_URL = "ws://127.0.0.1:8080/ws"
$env:AGENT_TOKEN = "<coordinator JWT>"
$env:AGENT_ID = "claude_pair1_coordinator"
$env:AGENT_NAME = "Claude Pair 1 Coordinator"
$env:AGENT_TYPE = "coordinator"
$env:AGENT_PROJECT_ID = "project_example"
$env:AGENT_PEER_ID = "claude_pair1_worker"
claude

Give it this role instruction:

You are Agent A, the coordinator. Use the agent_hub MCP tools for external delegation. When another agent would materially help, call delegate_task with a precise task, complete context, and timeoutSeconds 900. Do not claim to be waiting unless you actually called the tool. Verify the returned work before answering the user. At the start of each new turn, before anything else, call wait_for_message with timeoutSeconds 2 as routine housekeeping to silently pick up anything Agent B sent while you were idle; a timeout there is normal.

5. Open Claude Agent B

Open a second terminal in the same target project and use the worker's distinct identity:

$env:AGENT_HUB_URL = "ws://127.0.0.1:8080/ws"
$env:AGENT_TOKEN = "<worker JWT>"
$env:AGENT_ID = "claude_pair1_worker"
$env:AGENT_NAME = "Claude Pair 1 Worker"
$env:AGENT_TYPE = "worker"
$env:AGENT_PROJECT_ID = "project_example"
$env:AGENT_PEER_ID = "claude_pair1_coordinator"
claude

Give it this role instruction:

You are Agent B, the worker. Immediately call agent_hub.wait_for_task with timeoutSeconds 900. When it returns task_received, continue in this same turn and execute the task. Call agent_hub.check_for_updates at safe checkpoints approximately every 30 seconds, before mutations, between bounded batches, after long-running tools, and before sending a result. Apply updates before continuing. If cancellation is returned, stop starting new work and call confirm_task_cancelled. Otherwise call send_task_result with the exact routing IDs, handle task_result_deferred by applying the update and retrying, then call wait_for_task again. Do not stop after merely displaying the received task. If wait_for_task instead returns message_received, that is a non-task message from Agent A, not work to execute; read it, respond with send_message if appropriate, and immediately call wait_for_task again in the same turn. Also check every tool result's content for a pendingPeerMessages field and read any messages listed there before moving on.

Exact mixed pair: Codex Agent A + Claude Agent B

This is the recommended mixed arrangement: Codex owns the main user conversation and Claude Code waits for delegated work.

Use two unique identities, for example:

Agent A: mixed_pair1_codex
Agent B: mixed_pair1_claude
Project: project_mixed_demo

Provision both identities and memberships as described above, then issue their tokens:

$env:JWT_SECRET = "local-test-secret-change-this"
node "C:\Users\13072\Desktop\Python\websoket Agent communication\create-demo-agent-tokens.mjs" `
  "mixed_pair1_codex" `
  "mixed_pair1_claude"

Configure the Codex side

Create or extend .codex/config.toml in the target project. A project-scoped configuration applies after the repository is trusted.

[mcp_servers.agent_hub]
command = "node"
args = ["C:/Users/13072/Desktop/Python/websoket Agent communication/agent-mcp-server.mjs"]
env_vars = [
  "AGENT_HUB_URL",
  "AGENT_TOKEN",
  "AGENT_ID",
  "AGENT_NAME",
  "AGENT_TYPE",
  "AGENT_PROJECT_ID",
  "AGENT_PEER_ID",
  "AGENT_MCP_REQUEST_TIMEOUT_MS",
  "AGENT_RECONNECT_MAX_MS"
]
startup_timeout_sec = 30
tool_timeout_sec = 920

Open a terminal in the target project and launch Codex with the coordinator identity:

$env:AGENT_HUB_URL = "ws://127.0.0.1:8080/ws"
$env:AGENT_TOKEN = "<JWT printed as AGENT_CODEX_A_TOKEN>"
$env:AGENT_ID = "mixed_pair1_codex"
$env:AGENT_NAME = "Mixed Pair 1 Codex Coordinator"
$env:AGENT_TYPE = "coordinator"
$env:AGENT_PROJECT_ID = "project_mixed_demo"
$env:AGENT_PEER_ID = "mixed_pair1_claude"
$env:AGENT_MCP_REQUEST_TIMEOUT_MS = "900000"
$env:AGENT_RECONNECT_MAX_MS = "30000"
codex --no-alt-screen --cd .

Inside Codex, use /mcp to confirm agent_hub is connected. Then give Codex this coordinator instruction:

You are Agent A and Claude Code is external Agent B. Use agent_hub.delegate_task with timeoutSeconds 900 whenever an independent review or bounded parallel task would help. Include all paths, constraints, and acceptance criteria because Agent B cannot see this conversation. Do not use an internal subagent as a substitute. Verify Agent B's returned evidence before answering me. Before ending your turn, check the last tool result for a pendingPeerMessages field; that is the only place a post-task send_message from Agent B will appear. At the start of each new turn, before anything else, call wait_for_message with timeoutSeconds 2 as routine housekeeping to silently pick up anything Agent B sent while you were idle; a timeout there is normal.

Configure the Claude side

Use the .mcp.json configuration from the previous Claude section. Open a second terminal in the same project and launch Claude with the worker identity:

$env:AGENT_HUB_URL = "ws://127.0.0.1:8080/ws"
$env:AGENT_TOKEN = "<JWT printed as AGENT_CODEX_B_TOKEN>"
$env:AGENT_ID = "mixed_pair1_claude"
$env:AGENT_NAME = "Mixed Pair 1 Claude Worker"
$env:AGENT_TYPE = "worker"
$env:AGENT_PROJECT_ID = "project_mixed_demo"
$env:AGENT_PEER_ID = "mixed_pair1_codex"
$env:AGENT_MCP_REQUEST_TIMEOUT_MS = "900000"
$env:AGENT_RECONNECT_MAX_MS = "30000"
claude

Approve the project MCP server if prompted, confirm it with /mcp, and give Claude this worker instruction:

You are Agent B and Codex is external Agent A. Immediately call agent_hub.wait_for_task with timeoutSeconds 900. When a task arrives, continue in the same turn, execute it in this repository, and call check_for_updates with that task's correlationId at safe checkpoints. Apply every update before continuing. Call send_task_result with the exact routing IDs only after all updates are read, then call wait_for_task again. Never stop after only showing the received task. If wait_for_task instead returns message_received, that is a non-task message from Agent A; handle it and call wait_for_task again immediately in the same turn.

The communication sequence is:

User -> Codex A -> delegate_task -> hub -> Claude B
Claude B -> performs work -> send_task_result -> hub -> Codex A
Codex A -> verifies and answers user

While Claude B is working, Codex A can use send_update with the delegated task's correlationId. Claude B receives the update through check_for_updates or its normal wait_for_task call. send_update reports transport delivery separately from read acknowledgement; send_task_result is rejected until any unread update has been surfaced.

The current .bat launchers automate Codex-to-Codex pairs. This mixed configuration is manual; it does not yet automatically arrange the Codex and Claude windows on the secondary monitor. Do not run the normal Codex pair launcher and then reuse one of its IDs for Claude, because the duplicate connection will replace the original client.

Connecting another MCP-capable agent

Any client that supports local stdio MCP can use this hub:

  1. Provision its agent ID and project membership through the REST API.

  2. Issue a JWT using the same secret as the running hub.

  3. Configure a local stdio MCP server with command node and argument agent-mcp-server.mjs.

  4. Supply all required environment variables listed below.

  5. Instruct the client to use wait_for_task as a worker, call check_for_updates at safe checkpoints, or use delegate_task as a coordinator.

Required adapter environment:

Variable

Meaning

AGENT_HUB_URL

Hub WebSocket URL.

AGENT_TOKEN

JWT for this exact identity.

AGENT_ID

Unique registered agent ID.

AGENT_PROJECT_ID

Shared project ID.

AGENT_PEER_ID

The other member of this pair.

Optional environment:

Variable

Default

AGENT_NAME

Agent ID/client default

AGENT_TYPE

codex

AGENT_MCP_REQUEST_TIMEOUT_MS

900000 in the launchers

AGENT_RECONNECT_MAX_MS

30000

AGENT_HUB_MAILBOX_DIR

.mailbox/ next to agent-mcp-server.mjs

AGENT_HUB_NOTIFY

true (set to false to disable the Windows toast)

Connection lifetime and the 900-second limit

The 900-second value is the maximum duration of one delegate_task, wait_for_task, or wait_for_message call. It is not the lifetime of the WebSocket connection, MCP server, terminal, or AI session.

The connection can remain open as long as the processes remain running. After a wait times out:

  • Agent B can call wait_for_task again.

  • Agent A can retry a delegation only after checking whether Agent B completed or received the earlier request; do not blindly duplicate work.

  • The adapter reconnects to the hub after ordinary WebSocket interruptions using bounded exponential backoff.

For Claude Code, the .mcp.json example sets a per-tool timeout slightly above the hub's 900-second maximum. A local stdio MCP process that exits is normally restored by starting a new Claude session or reconnecting it from Claude's MCP controls.

Timeout and burst verification — 2026-08-19

The timeout-recovery behavior was verified with short deterministic wait windows that exercise the same code paths as the 900-second production window:

  • Agent A's result wait expired while Agent B remained active; Agent A resumed with the same correlation ID and recovered the late result.

  • Agent B's idle task wait expired while Agent A remained active; Agent B opened a new wait and completed a later delegation.

  • Agent A's result wait and Agent B's task wait expired simultaneously; both remained usable afterward.

  • After the simultaneous timeouts, 40 concurrent delegations and 40 corresponding results completed with unique, correctly matched correlation IDs.

The first burst run exposed an O(n²) duplicate-send problem: every new send re-flushed all earlier unacknowledged frames, eventually triggering the hub's pending-frame protection and WebSocket close code 1013. Both client implementations now mark each pending frame with its WebSocket connection generation. A frame is sent once on the current connection and may be resent once on a later connection if it was not acknowledged before reconnecting.

The full build and automated suite passed after the fix: 12 tests passed, 0 failed. The regression coverage is in test/hub.test.ts.

Context and persistence

There are three different kinds of context:

  1. Repository context: files such as this README and AGENTS.md; future agents can read these after a restart.

  2. Model conversation context: private to each Codex or Claude session; it is not shared automatically.

  3. Hub message context: the task and context fields included in a delegation.

The local launcher uses STORE_MODE=in-memory. Its registry and queued messages disappear when the hub stops. The launchers recreate their projects and memberships, but they do not restore old conversations. Use the Prisma/PostgreSQL mode if durable hub records are required.

Troubleshooting

AUTHENTICATION_FAILED / HTTP 401

The token was signed with a different secret, its subject does not equal AGENT_ID, or the identity was not registered. Confirm:

  • The hub and token helper used the same JWT_SECRET.

  • The JWT belongs to this agent ID.

  • The agent exists in the hub registry.

  • You are not sending placeholder or truncated token text.

MCP initialize response / connection closed

The stdio adapter exited before completing its MCP handshake. Run these checks:

node --check "C:\Users\13072\Desktop\Python\websoket Agent communication\agent-mcp-server.mjs"
Test-Path "C:\Users\13072\Desktop\Python\websoket Agent communication\agent-mcp-server.mjs"

Then verify all five required AGENT_* variables are present in that client session. The MCP server writes protocol messages to stdout, so do not add ordinary stdout logging to it.

A peer's send_message shows delivered/acknowledged in the hub log but the other agent never reacted

Hub-level delivered/acknowledged only proves the bytes reached the peer's local MCP process; it says nothing about whether that agent's model turn actually consumed the message. Before this was fixed, a plain message only surfaced through an explicit wait_for_message call, which neither the worker's task loop nor the coordinator's normal flow ever made — so pings sent between tasks were queued forever and silently missed, even though the hub reported them as delivered. As of the fix described above, a worker sitting in wait_for_task is interrupted immediately (status: "message_received"), and any other agent picks up a queued peer message as a pendingPeerMessages field on the result of its next tool call. If an agent still misses a message, confirm its MCP process did not exit between the send and its next tool call — a queued message does not survive a process restart under STORE_MODE=in-memory.

Agent B says it is waiting, but no task arrives

Check Agent A's transcript. Agent A must actually call agent_hub.delegate_task; writing a plan item or saying “waiting for Agent B” sends nothing.

Agent B displays the task but does not execute it

Reinforce the worker instruction: continue in the same turn, perform the work, call send_task_result with the returned routing IDs, then wait again.

Close code 4001 / replaced connection

Two live clients used the same agent ID. Keep the newest intended session and close the stale one. agent-instance.mjs deliberately does not reconnect a client that the hub replaced.

Delegation points at the wrong repository

The two terminals were launched from the wrong folder, or the delegation context named the wrong path. Verify the visible current working directory in both sessions and include the absolute target path in the delegated context.

No secondary monitor

The supply-chain launcher falls back to two separate terminal windows. This does not affect communication.

Hub state was lost

In-memory mode resets on hub restart. Run the launcher again so it reprovisions the pair, or use the persistent Prisma/PostgreSQL deployment.

Manual development commands

Install and build:

npm install
npm run build

Validate the project:

npm run build
npm test
node --check .\agent-instance.mjs
node --check .\agent-mcp-server.mjs
cmd.exe /d /c open-multi-agent-pair.bat --help
cmd.exe /d /c open-multi-agent-pair.bat --diagnose

Health and diagnostics:

Invoke-RestMethod http://127.0.0.1:8080/health
Invoke-RestMethod http://127.0.0.1:8080/ready
# /metrics additionally requires the central administrative token.

Important files

File

Purpose

src/server.ts

Starts the Fastify/WebSocket hub.

src/app.ts

REST endpoints and WebSocket registration.

src/sdk/agent-client.ts

TypeScript client SDK.

agent-instance.mjs

Portable WebSocket client and Codex pair/window launcher.

agent-mcp-server.mjs

Stdio MCP-to-WebSocket adapter used by AI clients.

create-demo-agent-tokens.mjs

Issues two local development JWTs.

ensure-local-secrets.mjs

Creates the ACL-restricted local launcher secrets file.

launch-project-agent-pair.ps1

Canonical validation, provisioning, launch, and cleanup implementation.

open-codex-pair.bat

Original seeded Codex demo launcher.

open-multi-agent-pair.bat

Thin wrapper that can be copied as the only launcher file in any project.

prisma/schema.prisma

Persistent data model.

.env.example

Hub configuration template.

Upstream client documentation

If either client changes its MCP configuration format, verify these pages and then update the client-specific sections of this README. The hub-side environment contract remains defined by agent-mcp-server.mjs.

Production notes

The generated local secrets and in-memory mode in the local launchers are for local testing only. The secret file is ACL-restricted to the current Windows account and ignored by Git and Docker. The launchers bind the hub to loopback, do not place JWTs in child command lines, close terminal shells when Codex exits, and stop a launcher-owned hub after the last WebSocket disconnects. Message bodies are omitted from the portable runner's console output unless AGENT_LOG_MESSAGE_CONTENT=true is explicitly set.

For a persistent or shared deployment:

  • Use STORE_MODE=prisma with PostgreSQL and Redis.

  • Replace JWT_SECRET and ADMIN_TOKEN with strong secrets.

  • Put TLS in front of the hub, use wss://, and set TLS_TERMINATED_UPSTREAM=true only for that protected deployment.

  • Set an explicit CORS_ORIGINS allowlist if browser origins need REST access; cross-origin access is disabled by default.

  • Authenticate /metrics with the administrative token and restrict it at the network layer.

  • Keep agents isolated by project membership and permissions.

  • Store large files externally and send paths, IDs, URLs, and hashes rather than binary payloads.

  • Do not expose the administrative REST API or development tokens publicly.

The full WebSocket envelope and persistence architecture are implemented in src, while this README documents the currently tested AI-agent operating workflow.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables MCP-capable agents running in separate consoles to exchange direct and broadcast messages through a blocking receive tool that releases instantly when mail arrives, so delivery feels push-like rather than polled. It also provides a live browser dashboard showing message flow, presence and per-agent profiles, plus CLI and HTTP endpoints for sending, tailing and replaying the log.
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    A task-level STDIO MCP server that lets Codex or any other MCP client hand off scoped coding jobs to an asynchronous worker agent which reads the code, edits files, and runs tests, while the client keeps ownership of planning and acceptance. Exposes submit, wait, query, follow-up, and cancel tools so multiple clients can queue and monitor tasks against a chosen project root.
    5
    1
    MIT