Overview
Codaph provides two direct capture adapters for running Codex agents and capturing their sessions in real-time:
codaph run — Uses Codex SDK (@openai/codex-sdk) for programmatic agent execution
codaph exec — Wraps codex exec --json CLI output for streaming capture
Both adapters stream events to the local .codaph mirror and Mubit cloud as the agent runs.
Most users should use codaph push to import agent history after running agents normally. Direct capture is for advanced workflows requiring inline capture.
Codex SDK Adapter (codaph run)
Overview
The Codex SDK adapter uses the @openai/codex-sdk npm package to run agents programmatically and capture events.
Source: src/lib/adapter-codex-sdk.ts:58
Installation
npm install @openai/codex-sdk
Usage
codaph run "fix the login bug"
Options:
--cwd <path> — Working directory for agent execution
--model <name> — Codex model to use
--resume <threadId> — Resume existing thread
--mubit — Enable Mubit cloud sync (default if MUBIT_API_KEY set)
--local-only — Disable Mubit, write only to local mirror
How It Works
- Initialize SDK: Detects
codex binary path via CODAPH_CODEX_PATH or which codex
- Start thread: Creates new thread or resumes existing via
resumeThreadId
- Stream events: Listens to
runStreamed() async iterator
- Ingest pipeline: Each event flows through:
- Redaction (
src/lib/security.ts:26) — strips sensitive data
- Canonicalization (
src/lib/core-types.ts:130) — adds eventId, actorId, timestamps
- Mirror write (
src/lib/mirror-jsonl.ts:74) — appends to local JSONL
- Mubit write (
src/lib/memory-mubit.ts:143) — pushes to cloud (if enabled)
- Extract response: Captures final agent message from
item.completed event
Example Session
codaph run "add unit tests for auth module" --model codex-base
Output:
codaph run: session abc123de started
codaph run: thread thread_xyz789 created
[agent executes...]
codaph run: final response:
I've added comprehensive unit tests for the auth module.
- Created tests/auth.test.ts with 12 test cases
- Added mocks for OAuth provider
- Achieved 94% code coverage
Architecture
// src/lib/adapter-codex-sdk.ts:73
async runAndCapture(
options: AdapterRunOptions,
onEvent?: (event: CapturedEventEnvelope) => Promise<void> | void,
): Promise<AdapterRunResult> {
const sessionId = randomUUID();
const repoId = options.repoId ?? repoIdFromPath(options.cwd);
let sequence = 0;
let threadId: string | null = options.resumeThreadId ?? null;
let finalResponse: string | null = null;
// Ingest prompt event
sequence += 1;
const promptEvent = await this.pipeline.ingest(
"prompt.submitted",
{ prompt: options.prompt, model: options.model ?? null, resumeThreadId: options.resumeThreadId ?? null },
{ source: "codex_sdk", repoId, sessionId, threadId, sequence },
);
if (onEvent) await onEvent(promptEvent);
// Start or resume thread
const thread = options.resumeThreadId
? this.codex.resumeThread(options.resumeThreadId, toThreadOptions(options))
: this.codex.startThread(toThreadOptions(options));
const streamed = await thread.runStreamed(options.prompt);
// Stream events
for await (const event of streamed.events) {
if (event.type === "thread.started") {
threadId = event.thread_id;
}
sequence += 1;
await this.pipeline.ingestRawLine(sessionId, JSON.stringify(event));
const captured = await this.pipeline.ingest(
event.type,
event as unknown as Record<string, unknown>,
{ source: "codex_sdk", repoId, sessionId, threadId, sequence },
);
const maybeFinal = extractFinalResponse(event);
if (maybeFinal) finalResponse = maybeFinal;
if (onEvent) await onEvent(captured);
}
return { sessionId, threadId, finalResponse };
}
Source: src/lib/adapter-codex-sdk.ts:73
Codex Path Detection
The adapter resolves the Codex binary path:
function resolveCodexPathOverride(): string | undefined {
const envOverride = process.env.CODAPH_CODEX_PATH ?? process.env.CODEX_PATH;
if (envOverride && envOverride.trim().length > 0) {
return envOverride.trim();
}
const lookupCmd = process.platform === "win32" ? "where" : "which";
const lookup = spawnSync(lookupCmd, ["codex"], { encoding: "utf8" });
if (lookup.status !== 0) return undefined;
const firstLine = lookup.stdout
.split(/\r?\n/)
.map((line) => line.trim())
.find((line) => line.length > 0);
return firstLine;
}
Source: src/lib/adapter-codex-sdk.ts:18
Priority:
CODAPH_CODEX_PATH environment variable
CODEX_PATH environment variable
which codex (or where codex on Windows)
Codex Exec Adapter (codaph exec)
Overview
The Codex exec adapter wraps codex exec --json and parses its streaming JSON output.
Source: src/lib/adapter-codex-exec.ts:50
Usage
codaph exec "refactor the database layer"
Options:
--cwd <path> — Working directory for agent execution
--model <name> — Codex model to use
--resume <threadId> — Resume existing thread
--mubit — Enable Mubit cloud sync
--local-only — Disable Mubit
How It Works
- Build args: Constructs
codex exec command with --json flag
- Spawn process: Launches
codex exec as child process
- Parse JSON lines: Reads stdout line-by-line using
readline.createInterface
- Ingest events: Each valid JSON line flows through the same pipeline as SDK adapter
- Handle errors: Stderr captured and logged; non-zero exit codes throw error
Command Construction
function buildArgs(options: AdapterRunOptions): string[] {
if (options.resumeThreadId) {
const args = ["exec", "resume", options.resumeThreadId, "--json", "--cd", options.cwd];
if (options.model) {
args.push("--model", options.model);
}
args.push(options.prompt);
return args;
}
const args = ["exec", "--json", "--cd", options.cwd];
if (options.model) {
args.push("--model", options.model);
}
args.push(options.prompt);
return args;
}
Source: src/lib/adapter-codex-exec.ts:32
Generated command:
codex exec --json --cd /path/to/project "your prompt here"
JSON Line Parsing
export function parseExecJsonLine(line: string): ParsedExecLine {
try {
const parsed = JSON.parse(line) as Record<string, unknown>;
if (!parsed.type || typeof parsed.type !== "string") {
return { ok: false, error: "Missing event type" };
}
return { ok: true, event: parsed };
} catch (error) {
return {
ok: false,
error: error instanceof Error ? error.message : "Invalid JSON line",
};
}
}
Source: src/lib/adapter-codex-exec.ts:17
Each line must:
- Be valid JSON
- Have a
type field (event type)
Invalid lines are logged as errors but don’t halt capture.
Event Stream Processing
const child = spawn("codex", args, {
cwd: options.cwd,
stdio: ["ignore", "pipe", "pipe"],
});
const stderrChunks: string[] = [];
child.stderr.on("data", (chunk: Buffer) => {
stderrChunks.push(chunk.toString("utf8"));
});
const reader = readline.createInterface({ input: child.stdout });
for await (const line of reader) {
const trimmed = line.trim();
if (!trimmed) continue;
await this.pipeline.ingestRawLine(sessionId, trimmed);
const parsed = parseExecJsonLine(trimmed);
if (!parsed.ok) {
sequence += 1;
const captured = await this.pipeline.ingest(
"error",
{ message: parsed.error, raw: trimmed },
{ source: "codex_exec", repoId, sessionId, threadId, sequence },
);
if (onEvent) await onEvent(captured);
continue;
}
const eventType = parsed.event.type as string;
if (eventType === "thread.started" && typeof parsed.event.thread_id === "string") {
threadId = parsed.event.thread_id;
}
sequence += 1;
const captured = await this.pipeline.ingest(eventType, parsed.event, {
source: "codex_exec",
repoId,
sessionId,
threadId,
sequence,
});
if (onEvent) await onEvent(captured);
}
Source: src/lib/adapter-codex-exec.ts:85
Common Options
Both adapters support:
Working directory for agent execution (defaults to current directory)
Codex model name (e.g., codex-base, codex-plus)
Resume existing thread by thread ID
Enable Mubit cloud sync (default if MUBIT_API_KEY set)
Disable Mubit, write only to local .codaph mirror
Override Mubit API key for this session
Event Types Captured
Both adapters capture these event types:
prompt.submitted — Initial prompt
thread.started — Thread created (captures thread_id)
item.started — Agent starts processing
item.streaming — Streaming response chunks
item.completed — Agent completes response (captures final text)
tool.started — Tool execution begins
tool.completed — Tool execution completes
error — Errors during execution
When to Use Each Adapter
| Scenario | Use |
|---|
| One-off agent run with capture | codaph exec (simpler, wraps CLI) |
| Programmatic agent execution | codaph run (SDK-based, more control) |
| Existing Codex workflow | codaph push (import history after) |
| Custom integrations | Import adapters in your own code |
Example: Custom Integration
import { IngestPipeline } from "codaph/lib/ingest-pipeline";
import { JsonlMirror } from "codaph/lib/mirror-jsonl";
import { CodexSdkAdapter } from "codaph/lib/adapter-codex-sdk";
const mirror = new JsonlMirror(".codaph");
const pipeline = new IngestPipeline(mirror);
const adapter = new CodexSdkAdapter(pipeline);
const result = await adapter.runAndCapture({
prompt: "implement feature X",
cwd: process.cwd(),
});
console.log(`Session: ${result.sessionId}`);
console.log(`Thread: ${result.threadId}`);
console.log(`Response: ${result.finalResponse}`);
Troubleshooting
”codex command not found”
Ensure Codex is installed and in your PATH:
which codex
# Should output: /usr/local/bin/codex (or similar)
Or set explicit path:
export CODAPH_CODEX_PATH=/path/to/codex
codaph exec "..."
“Mubit write failed”
Check Mubit configuration:
Or disable Mubit for local-only capture:
codaph exec "..." --local-only
Invalid JSON in exec output
If codex exec --json outputs invalid JSON, events are logged as errors but capture continues. Check:
codex exec --json "test prompt"
Ensure output is valid JSON lines.
Implementation
Sources:
- SDK adapter:
src/lib/adapter-codex-sdk.ts:58
- Exec adapter:
src/lib/adapter-codex-exec.ts:50
- Ingest pipeline:
src/lib/ingest-pipeline.ts:14
- Core types:
src/lib/core-types.ts:18
Tests:
test/lib-adapter-codex-sdk.test.ts
test/lib-adapter-codex-exec.test.ts