UNPKG

@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.

139 lines • 7.38 kB
/** * CROSS-PROCESS WAKE EXCLUSION — one wake per message, however many bridges are listening. * * ── THE DEFECT (peer marvin-coder, 2026-09-05) ────────────────────────────────────────── * Four commits with identical titles on main, plus another session's note: "the duplicate-work * prediction came true — five identical commits". Mechanism, read out of the relay source rather * than inferred: `ConnectionManager` holds `sessionId -> peerId -> PeerConnection[]` — an ARRAY — * and `sendToPeer` iterates it, sending to EVERY connection. That array is deliberate (a reconnect * keeps the old socket briefly), but it means N bridge processes registered as the SAME peer each * receive the SAME message. Each consults its own in-memory queue — empty in each, because the * queue is per-process — and each runs `claude --resume <the same threadId>` in the same cwd. * N copies, same work, same files. * * The existing wake-inflight marker cannot help: rail B only WRITES it (auto-relay.ts:931) as a * signal to rail C, and never reads it back to decide whether to wake. * * ── WHY A FILE, AND WHY `wx` ───────────────────────────────────────────────────────────── * The competing writers are separate OS processes, so the exclusion must live outside all of them. * `writeFileSync(..., { flag: "wx" })` is an atomic exclusive create: exactly one caller can win, * with no read-modify-write window. That is the same primitive peer-registry already uses for * local locks, including its stale-holder detection via `process.kill(pid, 0)`. * * Homedir-keyed, never COGENT_STATE_PATH: the competing processes may be launched with different * env (the documented trap for COGENT_CHECK_ON_STOP), and an exclusion the other process cannot * see is not an exclusion. * * ── PER MESSAGE, NOT PER CWD ───────────────────────────────────────────────────────────── * A per-cwd lock would prevent more collisions, and would also LOSE messages: both processes * receive every message, so the loser of a cwd-wide lock would silently drop a message it was the * only one free to handle. Per-message dedups the actual defect (the same message delivered N * times) while letting two processes legitimately split two different messages. * * ── FAILS OPEN, DELIBERATELY ───────────────────────────────────────────────────────────── * If a claim cannot be written or read, we WAKE. Duplicated work is visible and recoverable; a * silently dropped message is neither, and silence is the worst failure mode in this codebase. * This trades the fix's guarantee for the product's guarantee, on purpose. */ import crypto from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { getConfig } from "../config.js"; import { logger } from "../logger.js"; /** Hard ceiling on how long one claim may suppress other processes. Mirrors INFLIGHT_MAX_MS. */ export const CLAIM_MAX_MS = 1_800_000; // 30 min /** Multiplier over COGENT_TIMEOUT_MS covering primary + rotated-session retry, as wake-inflight does. */ const CLAIM_TIMEOUT_FACTOR = 3; /** MUST match wake-inflight.ts hashCwd / 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/wake-claims/<sha256(cwd)[..16]>.<peerId>.<messageId>.json` */ export function wakeClaimPath(peerId, messageId, cwd = process.cwd()) { return path.join(os.homedir(), ".cogent", "wake-claims", `${hashCwd(cwd)}.${safe(peerId)}.${safe(messageId)}.json`); } function ttlMs() { let timeout = 300_000; try { const c = getConfig()?.COGENT_TIMEOUT_MS; if (typeof c === "number" && Number.isFinite(c) && c > 0) timeout = c; } catch { /* config not loaded — documented default */ } return Math.min(timeout * CLAIM_TIMEOUT_FACTOR, CLAIM_MAX_MS); } /** Is the recorded holder still a live process? Unknown/odd values count as DEAD (steal). */ function holderAlive(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"; } } /** * Try to become the single process that wakes for this message. * * Returns true when this process owns the wake (or when claiming was impossible — see FAILS OPEN). * Returns false ONLY when another LIVE, UNEXPIRED process demonstrably holds it. */ export function claimWake(peerId, messageId, cwd = process.cwd()) { const p = wakeClaimPath(peerId, messageId, cwd); const now = Date.now(); const body = () => JSON.stringify({ pid: process.pid, startedAt: now, expiresAt: now + ttlMs() }); try { fs.mkdirSync(path.dirname(p), { recursive: true }); try { fs.writeFileSync(p, body(), { flag: "wx" }); // atomic: exactly one winner return true; } catch (err) { if (err?.code !== "EEXIST") throw err; } // Someone holds it. Take it over ONLY if they are gone or their claim has expired — a claim // that outlives its holder would mute this peer for good, which is worse than duplicating. let held = null; try { held = JSON.parse(fs.readFileSync(p, "utf-8")); } catch { held = null; // corrupt → treat as abandoned } const expired = !held || typeof held.expiresAt !== "number" || held.expiresAt <= now; if (!held || expired || !holderAlive(held.pid)) { fs.writeFileSync(p, body()); // steal logger.debug(`wake-claim: took over an abandoned claim for ${messageId}`); return true; } logger.info(`Auto-relay: SKIPPING wake for message ${messageId} — pid ${held.pid} in this cwd already has it. ` + `Another bridge process is registered as "${peerId}"; without this both would resume the same ` + `session and duplicate the work.`); return false; } catch (err) { // FAIL OPEN. Never lose a message to a broken lock. logger.warn(`wake-claim: could not claim ${messageId} (${err.message}) — waking anyway`); return true; } } /** Release the claim. Best-effort; an orphan expires or is stolen when its holder dies. */ export function releaseWake(peerId, messageId, cwd = process.cwd()) { try { fs.unlinkSync(wakeClaimPath(peerId, messageId, cwd)); } catch { /* already gone, or never ours */ } } //# sourceMappingURL=wake-claim.js.map