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.

76 lines • 4.18 kB
/** * DURABLE POLL CURSOR — a bridge restart must not replay the channel's history. * * ── THE DEFECT (measured 2026-09-06) ──────────────────────────────────────────────────── * `lastMessageId` lived ONLY in memory (message-inbox.ts, ws-client.ts), and so did the 1000-id * `seenIds` dedup set. The relay's poll route is explicit about the consequence: "If lastMessageId * is not provided (first poll), returns all messages." * * So every bridge restart re-fetched the ENTIRE channel from the beginning. Answered frames are * skipped (auto-relay returns on `response !== null`), but broadcasts and unanswered frames are * not — they were re-injected on every restart. Observed directly: minutes after a restart this * machine was running `claude --resume … -p "[Cogent Bridge: broadcast from Cogent_Master …]"` for * a broadcast sent DAYS earlier. A peer independently reported 410 replayed items and one frame * arriving three times, correlating with restarts rather than with socket state. * * `src/watch/index.ts:85` already primes correctly ("seed lastMessageId + seen from the backlog * without notifying"). The bridge never got that treatment — same codebase, two clients, one primes. * * ── WHY PERSIST RATHER THAN PRIME-AND-DISCARD ─────────────────────────────────────────── * Priming (fetch the backlog, throw it away, start from now) is simpler and would also stop the * replay — but it SILENTLY DROPS messages that genuinely arrived while the agent was down, which * is the failure mode this codebase treats as worst. A durable cursor resumes exactly where the * agent left off: nothing replayed, nothing lost. * * Keyed on (sessionId, peerId): the cursor is a position in ONE channel's stream for ONE identity. * Not cwd — the same checkout can serve different channels over time, and the relay session is what * the cursor is actually a position in. * * Best-effort throughout. A cursor that cannot be read means "start from the beginning", which is * exactly today's behaviour; a cursor that cannot be written means the next restart replays. Either * degrades to the status quo and never blocks message flow. */ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { logger } from "../logger.js"; const safe = (s) => String(s).replace(/[^A-Za-z0-9._-]/g, "_"); /** `~/.cogent/poll-cursors/<sessionId>.<peerId>.json` */ export function pollCursorPath(sessionId, peerId) { return path.join(os.homedir(), ".cogent", "poll-cursors", `${safe(sessionId)}.${safe(peerId)}.json`); } /** The last message this peer consumed on this channel, or null when there is no record. */ export function loadPollCursor(sessionId, peerId) { if (!sessionId || !peerId) return null; try { const raw = JSON.parse(fs.readFileSync(pollCursorPath(sessionId, peerId), "utf-8")); return typeof raw?.lastMessageId === "string" && raw.lastMessageId ? raw.lastMessageId : null; } catch { return null; // absent or corrupt — start from the beginning, i.e. the old behaviour } } /** Record the last consumed message. Best-effort; a failure only costs a replay next restart. */ export function savePollCursor(sessionId, peerId, lastMessageId) { if (!sessionId || !peerId || !lastMessageId) return; const p = pollCursorPath(sessionId, peerId); try { fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, JSON.stringify({ lastMessageId, updatedAt: new Date().toISOString() })); } catch (err) { logger.debug(`poll-cursor: could not persist (${err.message})`); } } /** Forget the cursor for this identity. Used when a peer deregisters from a channel for good. */ export function clearPollCursor(sessionId, peerId) { try { fs.unlinkSync(pollCursorPath(sessionId, peerId)); } catch { /* never existed, or already gone */ } } //# sourceMappingURL=poll-cursor.js.map