@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.
122 lines • 5.5 kB
JavaScript
import { getConfig } from "../config.js";
import { execClaude, detectCurrentSession, validateSession, listCandidates, projectDirsFor, recordedCandidate } from "./cc-cli.js";
import { execCodex, detectCurrentCodexSession, validateCodexSession, listCodexCandidates, codexSessionsRoot } from "./codex-cli.js";
import { appServerDaemonRunning, ensureDaemon, execCodexViaAppServer, runningUnderAppServerDaemon } from "./codex-app-server.js";
/**
* Codex wake dispatch, honoring COGENT_CODEX_WAKE:
* - `auto` (DEFAULT): use the app-server rail IFF this bridge is running under a
* `codex … app-server` daemon (i.e. codex is `--remote`, e.g. launched via
* cogent-codex). That's the ONLY reliable signal — in `--remote` mode the daemon,
* not the codex TUI, spawns the plugin's MCP, so an env flag never reaches the
* bridge. Detected structurally via process ancestry → zero config, works no
* matter how codex was started.
* - `app-server`: force the rail (explicit).
* - `exec-resume`: never use it (explicit opt-out; original behavior byte-for-byte).
* In every case it GRACEFULLY FALLS BACK to `exec-resume` when the daemon can't be
* reached — a plain codex still degrades to the honest "queued" message.
*/
async function execCodexWake(sessionId, message, cwd, timeoutMs, idempotencyKey) {
const wake = getConfig().COGENT_CODEX_WAKE;
// `auto` takes the app-server rail when EITHER we were spawned under the daemon OR a daemon is
// already listening. Ancestry alone missed every bridge that runs outside a cogent-codex-launched
// codex (a monitor script, a systemd unit) — those fell back to `exec-resume`, which takes the
// thread's EXCLUSIVE WRITER and locks the human out of their own live session with
// `-32600 already has an active writer`. The daemon is a machine-wide singleton, so being its
// descendant was never the right question. Never STARTS a daemon: no daemon → unchanged.
const useAppServer = wake === "app-server" ||
(wake === "auto" &&
(runningUnderAppServerDaemon() || appServerDaemonRunning()));
if (useAppServer) {
try {
const sock = await ensureDaemon();
const result = await execCodexViaAppServer(sessionId, message, cwd, timeoutMs, { socketPath: sock, idempotencyKey });
if (!result.appServerUnavailable)
return result;
// daemon unreachable at RPC layer → fall through to exec-resume
}
catch {
// ensureDaemon failed (no daemon / socket never appeared) → fall through
}
}
return execCodex(sessionId, message, cwd, timeoutMs);
}
/**
* Dispatch a non-interactive "resume + send" to the local agent CLI based on
* the local peer's platform. Only "codex" diverges; everything else (and the
* non-CLI platforms, which never reach auto-relay) uses the Claude path.
*/
export function execRemote(platform, sessionId, message, cwd, timeoutMs, opts) {
switch (platform) {
case "codex":
return execCodexWake(sessionId, message, cwd, timeoutMs, opts?.idempotencyKey);
case "cc":
default:
return execClaude(sessionId, message, cwd, timeoutMs);
}
}
/** Platform-aware "find the current session id for this cwd". */
export function detectCurrentSessionFor(platform, cwd) {
switch (platform) {
case "codex":
return detectCurrentCodexSession(cwd);
case "cc":
default:
return detectCurrentSession(cwd);
}
}
/** Platform-aware "list all candidate sessions for this cwd" (name + mtime). */
export function listCandidatesFor(platform, cwd) {
switch (platform) {
case "codex":
return listCodexCandidates(cwd);
case "cc":
default:
return listCandidates(cwd);
}
}
/**
* Platform-aware "which session did this cwd's agent report for ITSELF".
*
* Layer 1 of session discovery: a fact published by the agent (via the plugin
* hook payload) rather than a path we derived. Only Claude Code publishes a
* transcript path today; Codex already matches its rollouts by recorded cwd, so
* it has no derivation to bypass. Returns null when nothing was recorded.
*/
export async function reportedSessionFor(platform, cwd) {
if (platform !== "cc")
return null;
try {
return (await recordedCandidate(cwd))?.sessionId ?? null;
}
catch {
return null;
}
}
/**
* Platform-aware "which directories would I scan for this cwd's sessions".
*
* Diagnostics only. A zero-candidate resolve used to be a dead end: the log said
* "no candidate sessions found for cwd" without ever naming the directory it
* looked in, so a cwd-representation mismatch and a genuinely empty machine were
* indistinguishable. Surfacing the probed path turns that into a one-glance fix.
*/
export function probePathsFor(platform, cwd) {
switch (platform) {
case "codex":
return Promise.resolve([codexSessionsRoot()]);
case "cc":
default:
return projectDirsFor(cwd);
}
}
/** Platform-aware "does this session id still exist on disk". */
export function validateSessionFor(platform, sessionId, cwd) {
switch (platform) {
case "codex":
return validateCodexSession(sessionId);
case "cc":
default:
return validateSession(sessionId, cwd);
}
}
//# sourceMappingURL=exec-remote.js.map