feat(cli): websocket client + MCP tool integration
broker-client: full WS client with hello handshake + ack, auto-reconnect with exponential backoff (1s → 30s capped), in-memory outbound queue (max 100) during reconnect, 500-entry push buffer for check_messages. MCP tool integration: - send_message: "slug:target" prefix or single-mesh fast path - check_messages: drains push buffers across all clients - set_status: fans manual override across all connected meshes - set_summary: stubbed (broker protocol extension needed) - list_peers: stubbed — lists connected mesh slugs + statuses manager module holds Map<meshId, BrokerClient>, starts on MCP server boot for every joined mesh in ~/.claudemesh/config.json. new CLI command: seed-test-mesh injects a mesh row for dev testing. also fixes a broker-side hello race: handleHello sent hello_ack before the caller closure assigned presenceId, so clients sending right after the ack hit the no_hello check. Fix: return presenceId, caller sets closure var, THEN sends hello_ack. Queue drain is fire-and-forget now. round-trip verified: two clients, A→B, push received with correct senderPubkey + ciphertext. 44/44 broker tests still pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -267,7 +267,7 @@ function sendError(
|
||||
async function handleHello(
|
||||
ws: WebSocket,
|
||||
hello: Extract<WSClientMessage, { type: "hello" }>,
|
||||
): Promise<string | null> {
|
||||
): Promise<{ presenceId: string; memberDisplayName: string } | null> {
|
||||
// Capacity check BEFORE touching DB.
|
||||
const existing = connectionsPerMesh.get(hello.meshId) ?? 0;
|
||||
if (existing >= env.MAX_CONNECTIONS_PER_MESH) {
|
||||
@@ -308,8 +308,12 @@ async function handleHello(
|
||||
presence_id: presenceId,
|
||||
session_id: hello.sessionId,
|
||||
});
|
||||
await maybePushQueuedMessages(presenceId);
|
||||
return presenceId;
|
||||
// Drain any queued messages in the background. The hello_ack is
|
||||
// sent by the CALLER after it assigns presenceId — sending it here
|
||||
// races the caller's closure assignment, causing subsequent client
|
||||
// messages to fail the "no_hello" check.
|
||||
void maybePushQueuedMessages(presenceId);
|
||||
return { presenceId, memberDisplayName: member.displayName };
|
||||
}
|
||||
|
||||
async function handleSend(
|
||||
@@ -348,7 +352,22 @@ function handleConnection(ws: WebSocket): void {
|
||||
try {
|
||||
const msg = JSON.parse(raw.toString()) as WSClientMessage;
|
||||
if (msg.type === "hello") {
|
||||
presenceId = await handleHello(ws, msg);
|
||||
const result = await handleHello(ws, msg);
|
||||
if (!result) return;
|
||||
presenceId = result.presenceId;
|
||||
// Ack AFTER closure assignment — subsequent client messages
|
||||
// arriving immediately after will now see a non-null presenceId.
|
||||
try {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "hello_ack",
|
||||
presenceId: result.presenceId,
|
||||
memberDisplayName: result.memberDisplayName,
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
/* ws closed during hello */
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!presenceId) {
|
||||
|
||||
@@ -95,6 +95,13 @@ export interface WSAckMessage {
|
||||
queued: boolean;
|
||||
}
|
||||
|
||||
/** Broker → client: hello handshake acknowledgement. */
|
||||
export interface WSHelloAckMessage {
|
||||
type: "hello_ack";
|
||||
presenceId: string;
|
||||
memberDisplayName: string;
|
||||
}
|
||||
|
||||
/** Broker → client: structured error. */
|
||||
export interface WSErrorMessage {
|
||||
type: "error";
|
||||
@@ -108,4 +115,8 @@ export type WSClientMessage =
|
||||
| WSSendMessage
|
||||
| WSSetStatusMessage;
|
||||
|
||||
export type WSServerMessage = WSPushMessage | WSAckMessage | WSErrorMessage;
|
||||
export type WSServerMessage =
|
||||
| WSHelloAckMessage
|
||||
| WSPushMessage
|
||||
| WSAckMessage
|
||||
| WSErrorMessage;
|
||||
|
||||
Reference in New Issue
Block a user