@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.
213 lines • 11.5 kB
TypeScript
import { type BridgeEntry, type OwnershipVerdict } from "./wake-ownership.js";
/** The states an agent can be in with respect to its channel. */
export type PresenceState = "local" | "deferred" | "offline" | "online" | "standby";
/**
* What the bridge holding the name is to this one, from the two agent-session ids (Claude Code
* passes one, codex and gemini pass none, and nothing before 3.25.7 recorded it):
* same-session both known, equal → this agent's own other bridge
* no-session neither side has one → this agent's previous bridge, or another agent (codex, gemini)
* other-session both known, differ → another agent, proven
* unknown only one side has one → this agent's own older bridge, or another agent
* 🔴 An ABSENT id is never "different" (review r5 SEC-2): the plugin 3.25.6 + `npx @latest` pair of
* ONE Claude session was told another agent session held its name.
*/
export type HolderRelation = "same-session" | "no-session" | "other-session" | "unknown";
export declare function holderRelation(holderSession: string | undefined, ownSession: string | undefined): HolderRelation;
/** The live bridge that already serves this peer (from its ownership announcement). */
export interface ServingBridge {
pid: number;
version?: string;
startedAt?: number;
/** The agent session it announced (3.25.7+, Claude Code); absent = cannot be compared. */
sessionId?: string;
}
/** One process as `ps` reports it (structural subset of stale-bridge-reaper's ProcInfo). */
export interface PsProc {
pid: number;
ppid: number;
argv: string;
}
/**
* 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 declare function holderClientGone(holder: {
pid: number;
parentPid?: number;
noClient?: boolean;
}, procs: PsProc[]): boolean;
/**
* 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 declare function insideCodexWakeTurn(selfPid: number, procs: PsProc[]): boolean;
/**
* 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 declare function otherBridgesInFolder(entries: BridgeEntry[], self: {
pid: number;
sessionId?: string;
},
/** Called at most once, and only when a candidate is left to check (one `ps`). */
listProcs: () => PsProc[]): BridgeEntry[];
/**
* 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 declare 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 declare function heldInFolder(others: BridgeEntry[], peerId: string, ownSession?: string): BridgeEntry | undefined;
/**
* 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 declare function mayBeOwnPreSessionBridge(holder: BridgeEntry, ownSession: string | undefined): boolean;
/**
* 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 declare function heldNameAdvice(peerId: string, holder?: {
sessionId?: string;
platform?: string;
}, own?: {
sessionId?: string;
platform?: string;
}): string;
/** True when one of them is PROVEN to be another agent session (both ids recorded, different). */
export declare function anotherAgentSessionAmong(others: BridgeEntry[], ownSession: string | undefined): boolean;
/**
* 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 declare function otherBridgeNotice(others: BridgeEntry[], peerId: string, verdict: OwnershipVerdict, ownSession?: string): string;
/** What a presence evaluation concluded, and what it did about it. */
export type PresenceOutcome = {
state: "local" | "deferred" | "online";
restored: false;
detail: string;
} | {
state: "online";
restored: true;
peerId: string;
detail: string;
} | {
state: "standby";
restored: false;
peerId: string;
servedBy: ServingBridge;
/** What servedBy is to this bridge, as far as the two agent-session ids can say. */
holder: HolderRelation;
/** True when the standby is re-checked and restores by itself (rule b); false = final (rule c). */
recheck: boolean;
detail: string;
} | {
state: "offline";
restored: false;
detail: string;
};
export interface PresenceDeps {
/** True only when the ACTIVE backend really is the cloud one — not a configuration flag. */
isCloudConfigured: () => boolean;
/** True when something already registered in THIS process; its state is authoritative. */
alreadyRegisteredInProcess: () => boolean;
/**
* Is a LIVE bridge already announcing this peer for this working directory?
*
* 🔴 TWO BRIDGES, ONE NAME. Credentials are one file per working directory, and every bridge
* in the checkout reads it — one Claude Code session can run two (plugin + npx), and two agents
* can share a checkout. Without this check the second bridge
* re-registers the first one's peerId and supersession stands one of them down. Proven by
* controlled A/B: with the restore unguarded, wake-dedup-matrix-test --topology two-peer could no
* longer detect its own break (STAYED_GREEN, 0 overlapping resumes); with presence off it went
* RED with 1. The gate was telling the truth.
*
* Returns that bridge, or null when nobody else serves the peer.
*/
peerAlreadyServed: (peerId: string) => ServingBridge | null;
/** The agent session this bridge serves (wake-ownership agentSessionId); undefined = not known. */
agentSessionId?: () => string | undefined;
/** True when this bridge runs inside a `codex exec resume` wake turn (insideCodexWakeTurn). */
startedByWakeTurn?: () => boolean;
loadCredentials: () => Promise<{
endpoint?: string;
sessionId?: string;
token?: string;
peerId?: string;
/** AUD-003 ownership proof, issued once when this peer was created. */
peerSecret?: string;
} | null>;
/**
* COG-80: why loadCredentials returned nothing although the folder has saved names (they belong to
* other agents, or several candidates and none is provably this agent's). undefined = no such case.
*/
restoreSkip?: () => string | undefined;
/** Go cloud-live and register, restoring the channel entry. Throws on failure. */
goOnline: (peer: {
peerId: string;
endpoint: string;
sessionId: string;
token: string;
cwd: string;
label: string;
/** Replayed on register so a peer owned by an EARLIER token can be reclaimed. */
peerSecret?: string;
}) => Promise<void>;
cwd: () => string;
/** How often a re-checked standby looks again, for the message only (startup.ts owns the timer). */
recheckSeconds?: number;
env?: NodeJS.ProcessEnv;
}
/**
* 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 declare function autoPresenceEnabled(env?: NodeJS.ProcessEnv): boolean;
/**
* 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 declare function restorePresence(deps: PresenceDeps): Promise<PresenceOutcome>;
/** Record the evaluated presence. Called by startup after each evaluation. */
export declare function setPresence(outcome: PresenceOutcome): void;
/** The agent's own view of its channel presence. Null until first evaluated. */
export declare function getPresence(): PresenceOutcome | null;
/** Report the outcome once, at a level that matches how much it matters. */
export declare function logPresence(outcome: PresenceOutcome): void;
//# sourceMappingURL=peer-presence.d.ts.map