@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.
109 lines • 4.69 kB
JavaScript
import { logger } from "../logger.js";
/**
* Module-scoped cache for the relay's `version` field (from GET /api/health).
*
* Rationale (VER-02, plan 17-03):
* - Operators need to see "is my relay current?" at a glance via cogent_health_check.
* - A hot-path HTTP call on every health_check is too expensive.
* - A 60s TTL bounds staleness: relay upgrades become visible to the client
* within 60s without a client restart, but the fetch is amortized across
* N calls within the window.
*
* Failure semantics:
* - On HTTP error: return null, DO NOT cache. Next call retries.
* - On malformed payload (no `version` string): return null, DO NOT cache.
* - Caller (health-check tool) treats null as "unknown" — does NOT propagate
* the fetch failure upward; the rest of the health_check response is intact.
*
* Test seam:
* - `nowFn` parameter (default `Date.now`) lets tests advance the clock
* deterministically without coupling to vitest fake-timer state shared
* across other suites.
* - `__resetCacheForTest()` clears module state between tests.
*/
const TTL_MS = 60_000;
let cached = null;
/**
* Get the relay's version, using a 60s TTL cache. Returns null on fetch
* failure or malformed response. Does not throw.
*
* @param http Authenticated HttpClient (typically `HttpBackend.http`).
* @param nowFn Override for `Date.now()` (test seam). Defaults to `Date.now`.
*/
export async function getRelayVersion(http, nowFn = Date.now) {
const now = nowFn();
if (cached !== null && now - cached.fetchedAt < TTL_MS) {
return cached.version;
}
return (await _refresh(http, now))?.version ?? null;
}
/**
* Fetch and cache /api/health. Returns null (and caches nothing) on transport failure or a
* payload with no usable `version`, so the next call retries.
*
* Shared by getRelayVersion and getRelayCapabilities so gating a wire field costs no extra
* request — both read the same 60s-TTL entry.
*/
async function _refresh(http, now) {
try {
const resp = await http.get("/api/health");
const version = resp?.version;
if (typeof version !== "string" || version.length === 0) {
// Malformed response shape — treat as fetch failure. Do not cache.
logger.debug(`relay-version-cache: /api/health response missing string 'version' field`);
return null;
}
// A relay older than 2026-09-03 advertises nothing. That is a VALID answer meaning "supports
// no new wire contracts", not a failure — so it is cached like any other. Anything that is
// not an array of strings is ignored rather than trusted: a malformed field must never be
// read as support, because the cost of a false positive is a 400 on every send.
const raw = resp?.capabilities;
const capabilities = Array.isArray(raw)
? raw.filter((c) => typeof c === "string")
: [];
cached = { version, capabilities, fetchedAt: now };
return cached;
}
catch (err) {
const msg = err instanceof Error ? err.message : String(err);
logger.debug(`relay-version-cache: /api/health fetch failed: ${msg}`);
// Do NOT update `cached` — next call retries.
return null;
}
}
/**
* Wire contracts the relay advertises, or null when the relay could not be reached.
*
* 🔴 null and [] MEAN DIFFERENT THINGS and callers must treat both as "no support":
* - `[]` the relay answered and named nothing (an older build).
* - `null` we do not know (unreachable / malformed).
* Use relaySupports() rather than inspecting this directly.
*/
export async function getRelayCapabilities(http, nowFn = Date.now) {
const now = nowFn();
if (cached !== null && now - cached.fetchedAt < TTL_MS) {
return cached.capabilities;
}
return (await _refresh(http, now))?.capabilities ?? null;
}
/**
* Does the relay accept this wire contract?
*
* 🔴 FAILS SAFE. Unknown is NOT support. The message POST schema is `.strict()`, so guessing wrong
* in the optimistic direction is a hard 400 on every send — the 2026-08-07 outage and the
* 2026-09-02 near-miss were both exactly that. Guessing wrong in this direction merely omits an
* optional field.
*/
export async function relaySupports(http, capability, nowFn = Date.now) {
return (await getRelayCapabilities(http, nowFn))?.includes(capability) ?? false;
}
/**
* Test-only helper to clear the module-scoped cache. Call from beforeEach
* in test suites that exercise the cache.
*
* @internal
*/
export function __resetCacheForTest() {
cached = null;
}
//# sourceMappingURL=relay-version-cache.js.map