@essentialai/cogent-bridge
Version:
Cogent Bridge — cross-agent comms for Claude Code, OpenAI Codex and Slack. Codex users: install with `curl -fsSL https://cogent.tools/install.sh | sh`, then start with `cogent-codex` for real-time peer wake.
375 lines • 21.8 kB
JavaScript
import { z } from "zod";
import { getBackend, isCloudMode, isCloudDeferred } from "../backend/index.js";
import { BridgeError, BridgeErrorCode, successResult, errorResult } from "../errors.js";
import { getConfig } from "../config.js";
import { logger } from "../logger.js";
import { autoRelay } from "../services/auto-relay.js";
const RETRY_TIMEOUT_MS = 30_000;
const ACK_TIMEOUT_MS = 15_000;
function formatBridgeMessage(fromLabel, fromPeerId, message) {
return (`[Cogent Bridge message from ${fromLabel} (${fromPeerId})]\n\n` +
`${message}\n\n` +
`---\n` +
`Respond directly to the message above. Your entire response is delivered automatically ` +
`to whoever should receive it — a broadcast question (to the whole channel) is answered to everyone; ` +
`a direct message, to the sender. You do NOT need to — and should NOT — call cogent_send_message or ` +
`any bridge tool to deliver it; just answer normally.`);
}
/**
* Best-effort live status string for a peer, using whatever the relay
* currently knows. Returns a short suffix the caller appends to a timeout
* message so the LLM does not hallucinate "offline" when the peer is in
* fact connected. Never throws: on any backend error, returns a neutral
* fallback.
*/
async function describePeerLiveness(peerId) {
try {
const peer = await getBackend().getPeer(peerId);
if (!peer || !peer.lastSeenAt) {
return `Live status of ${peerId} is unknown (relay returned no record).`;
}
const lastSeenMs = Date.parse(peer.lastSeenAt);
if (Number.isNaN(lastSeenMs)) {
return `Live status of ${peerId} is unknown.`;
}
const ageSec = Math.max(0, Math.round((Date.now() - lastSeenMs) / 1000));
return `${peerId} was last seen by the relay ${ageSec}s ago.`;
}
catch (err) {
logger.debug(`describePeerLiveness: getPeer failed for ${peerId}: ${err}`);
return `Live status of ${peerId} is unknown.`;
}
}
export function registerSendMessageTool(server) {
server.registerTool("cogent_send_message", {
title: "Send Message to Peer",
description: "Send a message from one registered peer to another. " +
"The message is relayed by resuming the target's Claude Code session " +
"via CLI subprocess. Returns the target's response.",
inputSchema: {
fromPeerId: z
.string()
.describe("Peer ID of the sender, e.g. 'backend'"),
toPeerId: z
.string()
.describe("Peer ID of the recipient, e.g. 'frontend'. Use '*' or 'broadcast' to send to all peers."),
message: z
.string()
.describe("The message content to send to the target peer"),
replyExpected: z
.boolean()
.optional()
.describe("Set false for a TERMINAL message — an acknowledgement, confirmation or FYI that " +
"conveys information and needs no answer. The recipient's check-on-stop hook then " +
"never counts it as owing a reply. Omit (or true) for anything that asks a " +
"question or expects a response. Without this, two agents acknowledging each " +
"other never terminate: every ACK is itself a directed message demanding an ACK."),
},
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: true,
},
}, async ({ fromPeerId, toPeerId, message, replyExpected }) => {
try {
// SECURITY: a cloud-configured agent that has not joined a channel yet
// runs on the local FileBackend placeholder over the host-global
// cogent-state.json. Without this guard, getPeer(toPeerId) would read
// ANOTHER project's peer (leaking its session UUID) and — in the
// local-mode branch, since isCloudMode() is false while deferred —
// execClaude() would RESUME that project's session with attacker text
// and return its output. Refuse before touching any peer. Belongs to no
// channel → nothing to send. See backend/isCloudDeferred.
if (isCloudDeferred(getConfig().COGENT_ENDPOINT)) {
return successResult({
success: false,
notJoined: true,
error: "Not connected to a channel yet — call cogent_join_session (or cogent_register_peer) first. Message not sent (cross-channel send is blocked until you join).",
});
}
const backend = getBackend();
const from = await backend.getPeer(fromPeerId);
if (!from) {
return errorResult(new BridgeError(BridgeErrorCode.PEER_NOT_FOUND, `Sender peer '${fromPeerId}' not registered`, "Register the peer first with cogent_register_peer"));
}
// Normalize broadcast aliases
const normalizedToPeerId = (toPeerId === "broadcast" || toPeerId === "*") ? "*" : toPeerId;
const isBroadcast = normalizedToPeerId === "*";
// Skip peer lookup for broadcast messages
const to = isBroadcast ? null : await backend.getPeer(normalizedToPeerId);
if (!isBroadcast && !to) {
return errorResult(new BridgeError(BridgeErrorCode.PEER_NOT_FOUND, `Target peer '${toPeerId}' not registered`, "Register the peer first with cogent_register_peer"));
}
// For broadcast: skip session validation and execClaude — just record the message
// The server handles fanout to all peers and adapters
if (isBroadcast) {
const startMs = Date.now();
try {
await backend.recordMessage({
fromPeerId,
toPeerId: normalizedToPeerId,
message,
response: null,
durationMs: Date.now() - startMs,
success: true,
error: null,
...(replyExpected === false ? { replyExpected: false } : {}),
});
return successResult({
success: true,
response: `Broadcast message sent to all peers in the session.`,
error: null,
durationMs: Date.now() - startMs,
});
}
catch (err) {
return errorResult(err);
}
}
// Fast-fail: check if the target session file exists on disk
const sessionValid = await backend.validateSession(to.sessionId, to.cwd);
if (!sessionValid) {
return errorResult(new BridgeError(BridgeErrorCode.CLI_EXEC_FAILED, `Session '${to.sessionId}' not found for peer '${normalizedToPeerId}'. The session may have ended or the ID is wrong`, `Ask '${normalizedToPeerId}' to re-register with their current session ID`));
}
const prefixed = formatBridgeMessage(from.label, fromPeerId, message);
const startMs = Date.now();
try {
let result = await backend.execClaude(to.sessionId, prefixed, to.cwd);
// On timeout, retry once with a shorter timeout
if (result.exitCode === null) {
logger.info(`Message to '${toPeerId}' timed out, retrying with ${RETRY_TIMEOUT_MS}ms timeout`);
result = await backend.execClaude(to.sessionId, prefixed, to.cwd, RETRY_TIMEOUT_MS);
}
const durationMs = Date.now() - startMs;
const success = result.exitCode === 0;
// Detect cloud mode to adjust response behavior.
// Use isCloudMode() (runtime backend check) instead of config.COGENT_ENDPOINT
// because reinitCloudBackend() can swap the backend after config is frozen.
const config = getConfig();
const isCloud = isCloudMode();
// Update sender lastSeenAt (sender is always active)
try {
await backend.updateLastSeen(fromPeerId);
}
catch (e) {
logger.error("Failed to update lastSeenAt for sender", { error: e });
}
// Update target lastSeenAt only on success
if (success) {
try {
await backend.updateLastSeen(toPeerId);
}
catch (e) {
logger.error("Failed to update lastSeenAt for target", { error: e });
}
}
// Cloud mode: two-stage waiting -- first delivery ack, then response.
// Stage 1: delivery ack (15s) confirms recipient's MCP received the message.
// Stage 2: response (COGENT_TIMEOUT_MS) waits for execClaude result.
if (isCloud && success) {
// Set up both waiters BEFORE sending to avoid race conditions.
// 260527-mkz (Lever 1): the inline response wait is bounded to a
// short fast-path window — min(COGENT_FASTPATH_MS, COGENT_TIMEOUT_MS).
// A reply within the window returns inline (quick CC<->CC turns);
// beyond it we return success+pending and let the late reply land
// via the existing inject-only path (expectation is retained on
// timeout by waitForResponse). COGENT_TIMEOUT_MS stays the ceiling.
const effectiveAckTimeout = Math.min(ACK_TIMEOUT_MS, config.COGENT_TIMEOUT_MS);
const fastPathMs = Math.min(config.COGENT_FASTPATH_MS, config.COGENT_TIMEOUT_MS);
// The RESPONSE waiter is peer-scoped FIFO and must exist before the send, because a
// fast reply can beat our POST response.
const responsePromise = autoRelay.waitForResponse(toPeerId, fastPathMs);
// 🔴 This promise is deliberately ABANDONED on several paths — the no-ack branch
// below returns without awaiting it, and cancelResponseWaiter/stop() reject it. An
// abandoned rejected promise is an UNHANDLED REJECTION: Node logs it, and under
// --unhandled-rejections=throw it takes the process down. Attaching a no-op handler
// marks it handled WITHOUT swallowing anything — every later .then/.catch still
// receives the rejection, because each gets its own.
//
// Found 2026-09-03 by the repaired E2E suite (AUD-011): seven of these were firing on
// every full run, and `npm test` was exiting 1 while its summary line still read
// "892 passed" — the count is a proxy, the exit code is the condition.
responsePromise.catch(() => { });
// Send message via cloud relay
let outboundRecord;
try {
outboundRecord = await backend.recordMessage({
fromPeerId,
toPeerId,
message,
response: null,
durationMs,
success: true,
error: null,
...(replyExpected === false ? { replyExpected: false } : {}),
});
}
catch (recordErr) {
autoRelay.clearExpectation(toPeerId);
throw recordErr;
}
// 🔴 AUD-002: the delivery-ack waiter is keyed by MESSAGE ID, so it can only be
// registered once the relay has assigned one. Registering after the send is safe
// because an ack that beats us is buffered by resolveDeliveryAck and consumed here —
// the race is handled by the buffer instead of by peer-keyed correlation, which was
// what let a second concurrent send report the first as delivered.
const ackPromise = autoRelay.waitForDeliveryAck(toPeerId, outboundRecord.id, effectiveAckTimeout);
// Wait for delivery ack first (fast-path: confirms recipient WS is live)
let ackReceived = false;
try {
await ackPromise;
ackReceived = true;
}
catch {
// No ack received -- WS may be temporarily down, but message is queued.
// Do NOT bail out: fall through and wait for the response via HTTP poll.
logger.warn(`[send-message] No delivery ack from ${normalizedToPeerId} within ${effectiveAckTimeout}ms — ` +
`waiting for response via poll fallback`);
}
if (!ackReceived) {
autoRelay.cancelResponseWaiter(toPeerId);
// No delivery ack within 15s. Do NOT assert "appears offline" --
// the recipient may be connected but momentarily not draining
// its WS read loop (e.g. during a synchronous execClaude). Tell
// the LLM what we actually know: the message is queued at the
// relay, the peer's last-seen-by-relay timestamp, and where to
// look for a reply.
//
// v3.2.1 (FIX-TIMEOUT-UX): return success:false with timedOut:true
// and put the explanation in `message`. The previous shape
// (success:true, response:<stub-text>) caused LLMs to mistake the
// bridge's stub for the recipient's actual reply.
const liveness = await describePeerLiveness(normalizedToPeerId);
const offlineMessage = `Message accepted by relay; no delivery acknowledgment from ${to.label} ` +
`(${normalizedToPeerId}) within ${effectiveAckTimeout}ms. ${liveness} ` +
`The recipient may have replied with cogent_send_message instead of the ` +
`synchronous response pattern. Use cogent_get_history to check for a reply.`;
// Leave the history record's response as null so it's not
// mistaken for the recipient's actual reply on later inspection.
if (backend.updateMessageResponse) {
try {
await backend.updateMessageResponse(outboundRecord.id, null, Date.now() - startMs);
}
catch {
logger.warn("[send-message] Failed to update message response in history (offline)");
}
}
return successResult({
success: false,
timedOut: true,
error: "DELIVERY_NO_ACK",
message: offlineMessage,
response: null,
durationMs: Date.now() - startMs,
});
}
// Ack received -- now wait for the actual response
try {
const cloudResponse = await responsePromise;
// Update the original message record with the actual response
if (backend.updateMessageResponse) {
try {
await backend.updateMessageResponse(outboundRecord.id, cloudResponse, Date.now() - startMs);
}
catch {
// Best-effort: response was already returned to the user
logger.warn("[send-message] Failed to update message response in history");
}
}
return successResult({
success: true,
response: cloudResponse,
error: null,
durationMs: Date.now() - startMs,
});
}
catch {
// 260527-mkz (Lever 1): ack received (recipient is online and
// got the message) but the reply did not arrive within the
// fast-path window. This is NOT a failure — it is a successful
// ASYNC delivery. The recipient serializes its turn and may
// still be working; the reply will land later and be injected
// via the existing inject-only path (handleIncoming), waking
// this agent. We MUST NOT clear or cancel the expectation here:
// waitForResponse already removed only its own (now-expired)
// waiter on timeout and KEPT expectingResponseFrom, so exactly
// one late _injectOnly fires. Calling clearExpectation /
// cancelResponseWaiter would drop that retained expectation and
// the late reply would be wrongly re-relayed (loop) or lost.
//
// Reframe the result: success:true + pending:true (no
// RESPONSE_TIMEOUT error, response stays null). This eliminates
// the false response:null failures for online recipients.
const fastPathSec = Math.round(fastPathMs / 1000);
const pendingMessage = `✓ Delivered to ${to.label} (${normalizedToPeerId}). They're working on it — ` +
`no reply within ${fastPathSec}s, but the reply will arrive as a separate ` +
`message (you'll be woken when it lands). Continue your work; don't re-send. ` +
`This is a successful async delivery, not a failure.`;
// Mark the history record as pending (success:true, response
// still null). Do NOT write it as an error. updateMessageResponse
// carries the pending flag through to the backend; the response
// stays null so it is never mistaken for the recipient's reply.
if (backend.updateMessageResponse) {
try {
await backend.updateMessageResponse(outboundRecord.id, null, Date.now() - startMs, true);
}
catch {
logger.warn("[send-message] Failed to mark message pending in history");
}
}
return successResult({
success: true,
pending: true,
response: null,
error: null,
message: pendingMessage,
durationMs: Date.now() - startMs,
});
}
}
// Local mode: record with synchronous response from execClaude
await backend.recordMessage({
fromPeerId,
toPeerId,
message,
response: success ? result.stdout : null,
durationMs,
success,
error: success ? null : result.stderr,
...(replyExpected === false ? { replyExpected: false } : {}),
});
return successResult({
success,
response: success ? result.stdout : null,
error: success ? null : result.stderr,
durationMs,
});
}
catch (err) {
const durationMs = Date.now() - startMs;
const errorMsg = err instanceof Error ? err.message : String(err);
try {
await backend.recordMessage({
fromPeerId,
toPeerId,
message,
response: null,
durationMs,
success: false,
error: errorMsg,
});
}
catch (recordErr) {
logger.error("Failed to record failed message", { error: recordErr });
}
return errorResult(err);
}
}
catch (err) {
logger.error("send-message failed", { error: err });
return errorResult(err);
}
});
}
//# sourceMappingURL=send-message.js.map