@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.
462 lines • 28.5 kB
JavaScript
// ---------------------------------------------------------------------------
// PEER PRESENCE — what state this agent is in on its channel, why, and getting
// it back to `online` without anyone being told to restart something.
//
// ── THE VOCABULARY, taken from what this codebase already says ──────────────
// "local" COGENT_LOCAL / no relay. Presence is not a concept here.
// "deferred" EXISTING TERM (backend-provider.ts): cloud-CONFIGURED but never joined —
// there are no stored credentials for this working directory. Nothing to
// restore; the agent must join a channel first.
// "offline" Stored credentials name a peer for this agent, but this process has not
// registered it. The bridge is running; the AGENT IS NOT IN THE CHANNEL. Other
// peers see it as absent and cannot address it.
// "online" Registered. The channel lists this agent and messages reach it.
// "standby" Stored credentials name a peer, but ANOTHER live bridge has announced it for
// this working directory, so this process does not register (that would take
// the identity from it). This process is NOT in the channel. It used to report
// "online" and log at DEBUG: marvin-coder's session bridge sat unregistered behind
// a dying 3.23.8 bridge (2026-10-13), and nothing said so.
// ONE RULE SET (startup.ts restorePeerPresence applies it; holderRelation below):
// a. no live holder ⇒ restore from the folder's credential file. A holder whose MCP
// client is GONE (an orphan, reparented) or that is DRAINING serves no agent and
// is not a holder.
// b. the holder is PROVEN to be this agent's own bridge — both announced the same
// agent session (plugin + npx, a restart of one Claude session) ⇒ stand by,
// RE-CHECK, restore once it is gone or draining.
// c. the holder is, or may be, ANOTHER agent — the two session ids differ, only one
// side has one, or NEITHER has one (codex, gemini) ⇒ stand by and STAY there,
// exactly as on main. 🔴 Round 5 re-checked here too, and a second window's idle
// bridge took the agent's name when the agent's window closed; the reopened agent
// then stood by behind it and its messages woke the other session (review r5
// R5-1). 🔴 Round 6 still re-checked when neither side had an id, and a second
// codex/gemini agent's bridge did the same (review r6 FIX1-2, SF-1). Nothing a
// session-less bridge can read tells its own previous bridge from another
// agent's, so a restarted codex agent whose old bridge lingers under the
// app-server daemon registers again by hand, as on main. Only an explicit
// cogent_register_peer takes a name from a bridge that may be another agent's.
//
// ── HOW AN AGENT ENDS UP `offline` — the mechanism, stated plainly ──────────
// Registration is an explicit act: something must call `cogent_register_peer`. The
// bridge starting does NOT register — it only publishes the tools. So every time an
// agent session starts, the bridge comes up holding valid credentials while the agent
// is absent from its own channel, and stays that way until the agent calls the tool.
// If the agent never calls it — it forgot, it crashed first, the session was resumed
// from a transcript that had already registered — the peer is never (re-)created, and
// the relay's stale sweep eventually deletes the entry left over from last time.
//
// That state used to be both SILENT and SELF-PERPETUATING, because every repair in
// this client sat behind a successful registration:
// - announceBridge() (auto-relay) — written at register time
// - reapStaleBridges() (auto-relay) — "only after a peer is genuinely registered"
// - triggerReRegistration()(startup) — reads in-memory state, null in a fresh process
// So when registration was the thing that did not happen, nothing recovered and nothing
// cleaned up. Measured 2026-09-14 on one checkout: 18 live bridges at 3.23.9, last
// successful register five days earlier, the peer absent from a healthy Team channel
// whose two other agents were present and waiting on it.
//
// ── WHY RESTORING IT IS SAFE ────────────────────────────────────────────────
// - Credentials are keyed PER WORKING DIRECTORY (~/.cogent/credentials/<sha256(cwd)[..16]>.json,
// commit f758d8ee). A bridge in project A cannot inherit project B's channel.
// - The agent-session id need not be correct here: auto-relay re-resolves it on every
// wake (auto-relay.ts, "Only adopt a resolved id") and register-peer itself falls
// back to the channel session id on an unresolved resolution.
// - `peerId` in the credential is the CONSENT SIGNAL. create/join write credentials
// without one, and deregister clears it, so a deliberate departure is never undone.
//
// ── WHY WE DO NOT JUST ANNOUNCE ─────────────────────────────────────────────
// Reap signal 4 skips any ANNOUNCED pid, reasoning that an announced bridge is 3.23.4+
// and stands down by itself. Announcing WITHOUT registering would make every leftover
// immortal: exempt from reaping, yet never superseding itself (supersession is only
// consulted on a wake, which a bridge with no peer never gets). The invariant is "a live
// bridge either REGISTERS — and so announces and can stand down — or it stays reapable."
// Restoring presence registers, so the announce it performs is correct. Since 3.25.7 that
// invariant is carried differently: a bridge exits when its MCP client goes away (index.ts), so a
// leftover no longer needs reaping, and every bridge is in the registry from process start under
// PROCESS_MARK (wake-ownership.ts) — never under a peer name it has not registered.
// ---------------------------------------------------------------------------
import { logger } from "../logger.js";
import { PROCESS_MARK, registryText } from "./wake-ownership.js";
export function holderRelation(holderSession, ownSession) {
if (holderSession && ownSession)
return holderSession === ownSession ? "same-session" : "other-session";
return holderSession || ownSession ? "unknown" : "no-session";
}
/** Process-tree roots an orphan is reparented to: init, launchd, a `systemd --user` subreaper. */
const ORPHAN_PARENT_RE = /^(init|launchd|systemd)$/;
/**
* The two wrappers between an npx-launched bridge and its MCP client, as `ps` shows them (measured
* 2026-10-13): `node …/.bin/cogent-bridge` ← `sh -c cogent-bridge` ← `npm exec @essentialai/cogent-bridge…`.
*/
const NPX_WRAPPER_RE = /^(?:\S*\/)?(?:sh|bash|dash) -c cogent-bridge\b|^npm exec @essentialai\/cogent-bridge/;
/**
* Is the MCP client of an announced bridge GONE? Then the bridge serves no agent — it is an orphan
* left draining or (pre-3.25.7) running forever, like marvin's script-spawned bridges — and must not
* keep a restarted agent out of its own channel.
*
* - 3.25.7+ announcements record `parentPid`: gone when the bridge's CURRENT parent differs from it
* (an orphan is reparented; the kernel never hands a live child to another parent otherwise).
* - older announcements carry none: gone when the parent is pid 1 or an init-like process.
* 🔴 Not "ppid === 1" alone: a Linux desktop reparents orphans to its `systemd --user`
* subreaper (measured on this dev box, 2026-10-13: an orphaned `sleep` got ppid 1615 = systemd).
* 🔴 And not the immediate parent alone (review r3 R3-5): an npx-launched bridge keeps its `sh`
* and `npm exec` wrappers when the client dies — the one reparented is npm. Walk up through them.
*
* Unknown (the holder is not in the process list, ps failed) ⇒ NOT gone: no proof ⇒ it still counts.
* A holder that announced it started WITHOUT a client (headless) cannot lose one ⇒ never gone.
*/
export function holderClientGone(holder, procs) {
if (holder.noClient === true)
return false;
const byPid = new Map(procs.map((p) => [p.pid, p]));
let cur = byPid.get(holder.pid);
if (!cur)
return false;
if (typeof holder.parentPid === "number")
return cur.ppid !== holder.parentPid;
for (let hops = 0; hops < 3; hops++) {
if (cur.ppid === 1)
return true;
const parent = byPid.get(cur.ppid);
if (!parent)
return false;
const exe = parent.argv.trim().split(/\s+/)[0] ?? "";
if (ORPHAN_PARENT_RE.test(exe.split("/").pop() ?? ""))
return true;
if (!NPX_WRAPPER_RE.test(parent.argv.trim()))
return false;
cur = parent;
}
return false;
}
/**
* Was this bridge started inside a `codex exec resume` wake turn? codex (unlike claude's
* --strict-mcp-config, measured: no bridge starts in a claude wake turn) starts the configured MCP
* servers for the resumed turn, so each codex wake spawns a bridge in the same checkout. It serves
* no agent: the bridge that is waking it does. It must never register as the peer (review r3 R3-1)
* and must not be reported as an agent locked out of its channel (review r2 NEW-4).
*/
export function insideCodexWakeTurn(selfPid, procs) {
const byPid = new Map(procs.map((p) => [p.pid, p]));
const seen = new Set();
let cur = byPid.get(selfPid)?.ppid;
while (cur && cur > 1 && !seen.has(cur)) {
seen.add(cur);
const p = byPid.get(cur);
if (!p)
break;
// `codex exec resume …`, `node …/codex.js exec resume …`, `…/vendor/…/codex exec resume …`
if (/(^|[\s/])codex(\.js)?\s+exec\s+resume(\s|$)/.test(p.argv))
return true;
cur = p.ppid;
}
return false;
}
/**
* The OTHER live bridges registered in this folder — what an explicit cogent_register_peer tells
* its caller about (otherBridgeNotice). Not counted: this process, a bridge that announced this same
* agent session (the agent's own second bridge), a draining bridge and one whose MCP client is gone
* (they serve no agent), and the process marks.
*/
export function otherBridgesInFolder(entries, self,
/** Called at most once, and only when a candidate is left to check (one `ps`). */
listProcs) {
let procs = null;
return entries.filter((e) => e.peerId !== PROCESS_MARK &&
e.pid !== self.pid &&
!e.draining &&
holderRelation(e.sessionId, self.sessionId) !== "same-session" &&
!holderClientGone(e, (procs ??= listProcs())));
}
/**
* COG-85: how long a closed agent's old connection can still hold its name. MEASURED 2026-10-15 (codex
* 0.162.1, Linux and macOS, probe MCP server): codex's app-server keeps a quit thread's MCP server
* alive for 60.0 s, then SIGTERMs it. Plain `codex` uses that daemon too. Gemini and `codex --no-daemon`
* end it at once. A name still held after this is held by another live connection.
*/
export const HOLDER_LINGER_SECONDS = 60;
/**
* The live bridge in this folder that holds `peerId`, from otherBridgesInFolder (so never this
* process, this agent session's own bridge, a draining one, or one whose client is gone).
*/
export function heldInFolder(others, peerId, ownSession) {
return others.find((o) => o.peerId === peerId && !mayBeOwnPreSessionBridge(o, ownSession));
}
/**
* A bridge older than 3.25.7 records no agent session at all. Beside a bridge that knows its Claude
* session it may be that session's OWN older bridge — the plugin's bridge next to an `npx @latest` one
* (r5 SEC-2) — which lives as long as the session, so "wait 60 s" would never free the name (side-agent
* review 2026-10-15). Not refused, as before COG-85. Only a plain x.y.z version below 3.25.7 qualifies:
* an unparseable one is treated as current.
*/
export function mayBeOwnPreSessionBridge(holder, ownSession) {
if (holder.sessionId || !ownSession)
return false;
const v = /^(\d+)\.(\d+)\.(\d+)$/.exec(String(holder.version ?? ""));
if (!v)
return false;
const [maj, min, pat] = [Number(v[1]), Number(v[2]), Number(v[3])];
return maj < 3 || (maj === 3 && (min < 25 || (min === 25 && pat < 7)));
}
/**
* What an agent locked out of a held name is told to do (COG-85, operator 2026-10-15: retry in 60+ s).
* 🔴 COG-80: "wait 60 s" is only true when the holder may be this agent's OWN previous connection. When
* it is provably ANOTHER agent's live bridge (another session, or another platform), waiting frees
* nothing — KLAIRE 2026-10-16: Codex was told to wait while Claude's live bridge held its name.
*/
export function heldNameAdvice(peerId, holder, own) {
const otherSession = !!(holder?.sessionId && own?.sessionId && holder.sessionId !== own.sessionId);
const otherPlatform = !!(holder?.platform && own?.platform && holder.platform !== own.platform);
if (otherSession || otherPlatform) {
const who = [holder?.platform && `a ${holder.platform} agent`, holder?.sessionId && `session ${holder.sessionId}`]
.filter(Boolean).join(", ");
return (`'${peerId}' is held by ANOTHER agent's live bridge in this folder (${who}). It keeps the name while that ` +
`agent runs, so waiting will not free it. Do not take the name and do not rename: tell the user which ` +
`agent holds '${peerId}' and ask them to have THAT agent run /cogent:register with ITS OWN peer name ` +
`(which releases '${peerId}'); after that, run /cogent:register again with the SAME peer name '${peerId}'`);
}
return (`If this agent was just restarted, its previous connection closes within ${HOLDER_LINGER_SECONDS} seconds: ` +
`wait ${HOLDER_LINGER_SECONDS}+ seconds, then run /cogent:register again with the SAME peer name '${peerId}'. ` +
`If '${peerId}' is still held after that, another live connection is using it — another agent, or an old ` +
`window of this one still open. Do not take the name and do not rename: ask the user`);
}
/** True when one of them is PROVEN to be another agent session (both ids recorded, different). */
export function anotherAgentSessionAmong(others, ownSession) {
return others.some((o) => holderRelation(o.sessionId, ownSession) === "other-session");
}
/**
* What the agent that just registered `peerId` is told about them, in plain words (operator,
* 2026-10-13: "notify"). The registration is NOT refused: a lingering old window must not lock an
* agent out of its own name. `verdict` is checkWakeOwnership AFTER this bridge announced: the
* newest PROCESS wakes for a name, which is not always the one that registered last.
*
* 🔴 "Another agent session" is said only when both session ids are recorded and differ. A bridge
* that recorded none is usually this agent's OWN — a restarted codex agent's previous thread, the
* plugin 3.25.6 bridge beside `npx @latest` — and was told two agents shared its folder (r5 SEC-2).
*/
export function otherBridgeNotice(others, peerId, verdict, ownSession) {
const lines = others.map((o) => {
// Registry text is quoted through registryText only: these sentences are read by the agent.
const version = registryText(o.version);
const who = holderRelation(o.sessionId, ownSession) === "other-session"
? `Another live agent session in this folder (bridge pid ${o.pid}, version ${version}, agent session ${registryText(o.sessionId)})`
: `Another live bridge in this folder (pid ${o.pid}, version ${version}; its agent session cannot be ` +
`compared with this one, so it may be this agent's own previous or second bridge)`;
if (o.peerId !== peerId) {
// 🔴 NOT "or waits behind it" (review r7 SEC2-5, measured): a standby behind another session is final.
return (`${who} is still registered as '${registryText(o.peerId)}'. It keeps that name while it runs, but the ` +
`folder's saved identity now points at this bridge ('${peerId}'): after a restart that bridge's ` +
`agent comes back as '${peerId}' only if this bridge is no longer running then; otherwise it ` +
`stays out of the channel until it registers again.`);
}
if (verdict.isOwner) {
return (`${who} was registered as '${peerId}'. It is now superseded: this bridge holds '${peerId}' and ` +
`that bridge has stopped answering for it.`);
}
// 🔴 NOT "it keeps answering" (review r5 SEC-1, measured): the newest process is the one that
// wakes, but this registration took the name on the relay, which refuses that bridge's replies.
// 🔴 "in this channel" (review r7 SEC2-5, measured): registered in ANOTHER channel, that bridge
// keeps answering there; the registry does not say which channel it is in.
return (`${who} is registered as '${peerId}' too. NOBODY answers for '${peerId}' in this channel now: the ` +
`bridge that started last (pid ${verdict.owner?.pid ?? "unknown"}) is the one that wakes its agent, ` +
`but in this channel the relay registration now belongs to this bridge, so the relay refuses that ` +
`bridge's replies here and this bridge does not wake. It stays that way until that bridge is closed ` +
`or its agent registers again. If that bridge is registered in another channel, it still answers there.`);
});
return (lines.join(" ") +
(anotherAgentSessionAmong(others, ownSession)
? ` Each agent in this folder keeps its own saved name (COG-80): after a restart it comes back under that name, not the other agent's.`
: ""));
}
/**
* Automatic presence restore. ON by default — a default-off self-heal heals nobody.
* Opt out with COGENT_AUTO_PRESENCE=false to observe the state without acting on it.
*/
export function autoPresenceEnabled(env = process.env) {
const v = env.COGENT_AUTO_PRESENCE;
if (v === undefined || v === "")
return true;
return !/^(0|false|no|off)$/i.test(v.trim());
}
/**
* Determine this agent's presence and, when it is `offline` with a peer it owns,
* bring it back `online`.
*
* NEVER THROWS. Runs fire-and-forget after the MCP transport connects — the
* `initialize` handshake must never wait on I/O (scripts/startup-nonblocking-test.mjs).
*/
export async function restorePresence(deps) {
try {
if (!deps.isCloudConfigured()) {
return { state: "local", restored: false, detail: "local mode — presence does not apply" };
}
if (deps.alreadyRegisteredInProcess()) {
return { state: "online", restored: false, detail: "registered by this process" };
}
const creds = await deps.loadCredentials();
const skip = creds ? undefined : deps.restoreSkip?.();
if (skip) {
return { state: "deferred", restored: false, detail: `nothing restored: ${skip}` };
}
if (!creds || !creds.endpoint || !creds.sessionId || !creds.token) {
return {
state: "deferred",
restored: false,
detail: "no stored channel credentials for this working directory — join a channel first",
};
}
if (!creds.peerId) {
// Credentials WITHOUT a peerId: created by join/create, or cleared by a deliberate
// deregister. Either way this agent is not meant to be in the channel right now.
return {
state: "deferred",
restored: false,
detail: "channel credentials exist but name no peer — nothing was registered here",
};
}
const peerId = creds.peerId;
// 🔴 BEFORE any holder check (review r3 R3-1): the bridge that woke this turn may be draining or
// orphaned — not a holder — and this one then fell through and registered as the peer.
if (deps.startedByWakeTurn?.()) {
return {
state: "deferred",
restored: false,
detail: `this bridge runs inside a codex wake turn ('codex exec resume'); peer '${peerId}' belongs ` +
`to the agent's own bridge, which started this turn — nothing to restore here`,
};
}
// Someone live is already this peer. Registering again would take the identity from them.
// So stand by — and SAY so: this process is not in the channel.
const served = deps.peerAlreadyServed(peerId);
if (served) {
const session = deps.agentSessionId?.();
const holder = holderRelation(served.sessionId, session);
// What is REPORTED (the detail, the log, the health check's servedBy) quotes registry text
// through registryText only; the comparison above read the recorded value itself.
const servedBy = {
...served,
...(served.version === undefined ? {} : { version: registryText(served.version) }),
...(served.sessionId === undefined ? {} : { sessionId: registryText(served.sessionId) }),
};
const started = Number.isFinite(servedBy.startedAt)
? new Date(servedBy.startedAt).toISOString()
: "unknown";
// Re-checked only behind a PROVEN own bridge (rule b). Everything else is final (rule c).
const recheck = holder === "same-session";
const every = deps.recheckSeconds ? ` (re-checked every ${deps.recheckSeconds}s)` : "";
// COG-85: register refuses while that bridge lives (PEER_HELD_IN_FOLDER), so the advice is to wait it out.
const take = heldNameAdvice(peerId);
const why = {
"same-session": `That bridge serves this same agent session (${session})`,
"no-session": `Neither bridge records an agent session (codex, gemini), so it cannot be told whether that ` +
`bridge is this agent's previous one or another agent's. ${take}`,
"other-session": `That bridge serves another agent session (${servedBy.sessionId}; ours: ${session}). ` +
heldNameAdvice(peerId, { sessionId: servedBy.sessionId }, { sessionId: session }),
unknown: `Its agent session cannot be compared with this one (that bridge: ${servedBy.sessionId ?? "not recorded"}; ` +
`this bridge: ${session ?? "not known"}): it may be this agent's own other bridge of an older ` +
`version, or another agent's. ${take}`,
};
return {
state: "standby",
restored: false,
peerId,
servedBy,
holder,
recheck,
// A final standby is evaluated once, so it says "when this bridge started": the health
// check keeps showing this text after that bridge is gone.
detail: `peer '${peerId}' ${recheck ? "is already served" : "was already served when this bridge started"} ` +
`by a live bridge for this working directory ` +
`(pid ${servedBy.pid}, version ${servedBy.version ?? "unknown"}, started ${started}); ` +
`this bridge has NOT registered and ` +
(recheck
? `restores '${peerId}' by itself once that bridge is gone${every}. `
: `does NOT take '${peerId}' by itself, also after that bridge is gone. `) +
why[holder],
};
}
if (!autoPresenceEnabled(deps.env ?? process.env)) {
// Report the state, take no action. This is the "observe, do not act" mode.
return {
state: "offline",
restored: false,
detail: `peer '${peerId}' is not registered and COGENT_AUTO_PRESENCE is off — this agent is absent from its channel`,
};
}
await deps.goOnline({
peerId,
endpoint: creds.endpoint,
sessionId: creds.sessionId,
token: creds.token,
cwd: deps.cwd(),
// 🔴 THE PEER'S DISPLAY LABEL IS NOT PERSISTED ANYWHERE — only the CHANNEL's is,
// in credentials.label (create-session.ts writes `label: data.label`; join-session
// writes the resolved channel label). Using that would label the agent after the
// channel: a restore once put "paymentorchestrator" beside a sibling called "POFE".
// peerId is the one identifier we hold that genuinely names this agent. The
// operator's chosen label returns on the next cogent_register_peer, which carries it.
label: peerId,
// 🔴 WITHOUT THIS THE RESTORE CANNOT RECLAIM ITS OWN PEER. A re-join issues a NEW
// token, and AUD-003 ownership is proven by the token that CREATED the peer or by
// the secret issued with it. Omitting it means an entry owned by an earlier token
// answers PEER_NOT_YOURS and the agent stays offline — precisely the state this
// module exists to leave. The secret is in the credential file; send it.
peerSecret: creds.peerSecret,
});
return {
state: "online",
restored: true,
peerId,
// Says what THIS process did. Whether the relay now lists the peer is the relay's
// state, and asserting it from here would be a success nobody measured.
detail: `re-registered peer '${peerId}' from stored credentials`,
};
}
catch (err) {
const reason = err instanceof Error ? err.message : String(err);
return {
state: "offline",
restored: false,
detail: `presence restore failed: ${reason}`,
};
}
}
/**
* Last evaluated presence, so cogent_health_check can report the agent's own state
* without re-probing the relay.
*
* It lives HERE, in the leaf module, and not in startup.ts: a tool importing startup.ts
* creates an import cycle (tools -> startup -> tools) and the imported binding reads back
* `undefined` at call time. That cost 26 health-check tests once; do not move it.
*/
let lastPresence = null;
/** Record the evaluated presence. Called by startup after each evaluation. */
export function setPresence(outcome) {
lastPresence = outcome;
}
/** The agent's own view of its channel presence. Null until first evaluated. */
export function getPresence() {
return lastPresence;
}
/** Report the outcome once, at a level that matches how much it matters. */
export function logPresence(outcome) {
if (outcome.state === "online" && outcome.restored) {
logger.info(`Presence: offline -> online — ${outcome.detail}. This agent is addressable on its ` +
`channel again, and leftover bridges for this checkout are now reapable.`);
}
else if (outcome.state === "offline") {
logger.warn(`Presence: OFFLINE — ${outcome.detail}.`);
}
else if (outcome.state === "standby" && outcome.holder === "other-session") {
// Another agent session holds the name, proven: this bridge's agent is absent from its channel.
logger.warn(`Presence: STANDBY — ${outcome.detail}.`);
}
else if (outcome.state === "standby" || outcome.state === "deferred") {
// A deliberate skip still decides whether this agent is reachable. DEBUG-only made both
// branches invisible at the default level (marvin 445619: nothing after "preflight OK").
logger.info(`Presence: ${outcome.state.toUpperCase()} — ${outcome.detail}.`);
}
else {
logger.debug(`Presence: ${outcome.state} — ${outcome.detail}.`);
}
}
//# sourceMappingURL=peer-presence.js.map