@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
JavaScript
/**
* 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