@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.
193 lines • 9.4 kB
JavaScript
/**
* WAKE OWNERSHIP — a superseded bridge process stops waking, by itself, with nobody running pkill.
*
* ── WHY (operator, 2026-09-05) ───────────────────────────────────────────────────────────
* "killed, but my users dont do that - so think how to have self healing mechanism"
*
* ── WHAT IS ACTUALLY ON A REAL MACHINE (measured here, 2026-09-05) ───────────────────────
* 37 live cogent-bridge processes, and NOT ONE of them is an orphan — every parent is alive:
*
* codex app-server (1 daemon, up 3.5 days) ......... 7 bridges, all at a stale 3.22.0
* claude --resume <one interactive session> ........ 2 bridges (plugin AND an npx @latest)
* 5 other interactive `claude --resume` sessions ... 1 each
*
* That kills the obvious fix. "Exit when stdin hits EOF" heals nothing here, because no bridge has
* lost its parent. They are not orphaned, they are REDUNDANT: a long-lived daemon spawns a bridge
* per MCP session and never reaps them, and one agent session can have cogent registered twice.
* A plugin update does not stop any of them, so old versions keep serving for days.
*
* ── THE SIGNAL: SAME peerId, NOT same cwd ────────────────────────────────────────────────
* Superseding on cwd alone would break a legitimate setup — two agents working one checkout (say
* `marvin-coder` and `marvin-mobile`) are two DIFFERENT peers and both must keep waking. What is
* never legitimate is the same peerId served by two bridges in one checkout: that is the ghost,
* and it is exactly what was measured on the real user (`marvin-coder` live on the FREE relay at
* v3.20.0 and on the TEAM relay at v3.23.1, same cwd, same thread).
*
* So ownership is keyed on (cwd, peerId). The wake LOCK (wake-lock.ts) stays keyed on cwd alone,
* because worktree corruption does not care which peer caused it. Two invariants, two keys:
* ownership → only ONE process does this peer's work (kills duplicate work)
* lock → only ONE turn runs in this checkout (kills concurrent corruption)
*
* ── NEWEST WINS, BY PROCESS START, SO IT CANNOT OSCILLATE ────────────────────────────────
* The owner is the live bridge with the latest PROCESS START time. Start time is fixed for the
* life of a process, so a superseded bridge can never win the title back — if ownership were keyed
* on registration time instead, a stood-down bridge that re-registered would steal it back and the
* two would ping-pong forever.
*
* ── ONE FILE PER PID, SO THERE IS NO WRITE RACE ──────────────────────────────────────────
* Each process writes ONLY its own `<pid>.json`; nobody rewrites a shared document, so concurrent
* registration cannot corrupt or lose an entry. Reading is a readdir plus a liveness check, and
* dead entries are pruned opportunistically by whoever notices them.
*
* ── FAILS OPEN ───────────────────────────────────────────────────────────────────────────
* Any unreadable/unwritable state resolves to "you are the owner, go ahead and wake". A
* self-healing mechanism that can silence a healthy agent is a worse bug than the one it heals —
* that is the 3.21.5 lesson, where a fix took auto-wake down for every default-config macOS user.
*/
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { logger } from "../logger.js";
/**
* When this process started. Captured at module load — as close to process start as we can get
* from inside — and never recomputed, which is what makes ownership stable and non-oscillating.
*/
const PROCESS_STARTED_AT = Date.now() - Math.floor(process.uptime() * 1000);
/** MUST match wake-lock / wake-claim / credential-store — one cwd, one key, everywhere. */
function hashCwd(cwd) {
return crypto.createHash("sha256").update(path.resolve(cwd)).digest("hex").slice(0, 16);
}
const safe = (s) => String(s).replace(/[^A-Za-z0-9._-]/g, "_");
/** `~/.cogent/bridges/<sha256(cwd)[..16]>.<peerId>/` — one directory per identity. */
export function bridgeDirFor(cwd, peerId) {
return path.join(os.homedir(), ".cogent", "bridges", `${hashCwd(cwd)}.${safe(peerId)}`);
}
/** Is that pid a live process? Unknown/odd values count as DEAD so their entries get pruned. */
function pidAlive(pid) {
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0)
return false;
if (pid === process.pid)
return true;
try {
process.kill(pid, 0); // signal 0 = existence check only
return true;
}
catch (err) {
// EPERM means it EXISTS but belongs to another user — alive for our purposes.
return err?.code === "EPERM";
}
}
/**
* Announce this process as a bridge serving `peerId` in `cwd`.
*
* Called once the peer is actually registered, so a bridge that never registered never competes
* for ownership. Best-effort: a failure here leaves us un-announced, which can only make us LOSE
* ownership to someone else, never wrongly take it.
*/
export function announceBridge(cwd, peerId, version, endpoint) {
const dir = bridgeDirFor(cwd, peerId);
try {
fs.mkdirSync(dir, { recursive: true });
const entry = {
pid: process.pid,
startedAt: PROCESS_STARTED_AT,
version,
peerId,
endpoint,
cwd: path.resolve(cwd),
};
fs.writeFileSync(path.join(dir, `${process.pid}.json`), JSON.stringify(entry));
}
catch (err) {
logger.debug(`wake-ownership: could not announce this bridge (${err.message})`);
}
}
/** Remove this process's announcement. Called on graceful shutdown; a crash is handled by pruning. */
export function withdrawBridge(cwd, peerId) {
try {
fs.unlinkSync(path.join(bridgeDirFor(cwd, peerId), `${process.pid}.json`));
}
catch {
/* already gone, or never announced */
}
}
/**
* Every LIVE bridge announced for this identity, newest process first.
*
* Prunes entries whose process is gone, so a machine that has been through a dozen plugin updates
* does not accumulate junk here forever.
*/
export function listLiveBridges(cwd, peerId) {
const dir = bridgeDirFor(cwd, peerId);
let names;
try {
names = fs.readdirSync(dir);
}
catch {
return []; // no directory = nobody announced = no competition
}
const live = [];
for (const name of names) {
if (!name.endsWith(".json"))
continue;
const p = path.join(dir, name);
let entry = null;
try {
entry = JSON.parse(fs.readFileSync(p, "utf-8"));
}
catch {
entry = null;
}
if (!entry || !pidAlive(entry.pid) || typeof entry.startedAt !== "number") {
try {
fs.unlinkSync(p); // prune: the process is gone or the record is unusable
}
catch {
/* raced with another pruner */
}
continue;
}
live.push(entry);
}
// Newest process first; pid breaks a same-millisecond tie deterministically so every process
// computes the SAME owner. A non-deterministic tie-break would let two bridges each believe
// they own it — the exact condition this exists to prevent.
live.sort((a, b) => b.startedAt - a.startedAt || b.pid - a.pid);
return live;
}
/**
* Should THIS process wake for `peerId` in `cwd`?
*
* No when a strictly newer live bridge serves the same identity — that bridge is the one the user
* most recently started, and we are the leftover.
*/
export function checkWakeOwnership(cwd, peerId) {
let live;
try {
live = listLiveBridges(cwd, peerId);
}
catch (err) {
logger.warn(`wake-ownership: could not read bridge registry (${err.message}) — assuming ownership`);
return { isOwner: true, liveCount: 1 };
}
// Nobody announced (including us — announcement is best-effort): assume ownership. Standing
// down on missing evidence would mute a healthy, solitary agent.
if (live.length === 0)
return { isOwner: true, liveCount: 0 };
const owner = live[0];
if (owner.pid === process.pid)
return { isOwner: true, liveCount: live.length };
// We are announced but are not newest -> superseded.
if (live.some((e) => e.pid === process.pid)) {
return {
isOwner: false,
owner: { pid: owner.pid, version: owner.version, endpoint: owner.endpoint },
liveCount: live.length,
};
}
// Others are announced but we are not (our own announce failed). Do NOT stand down on the
// strength of a record we could not write: fail open.
return { isOwner: true, liveCount: live.length + 1 };
}
//# sourceMappingURL=wake-ownership.js.map