Add optional @openai/codex-sdk exec-backed runner mode behind CODEX_RUNTIME=sdk, with CODEX_RUNTIME_SDK_ROLES canary limiting and app-server fallback for unsupported flows.
001: codex-sdk-runner
Question
Given EJClaw currently wraps codex app-server JSON-RPC directly, when we switch the runner core to the official @openai/codex-sdk, can we preserve the key runner guarantees: thread resume, final result extraction, cancellation, and structured event visibility?
Approach
- Added
@openai/codex-sdk@0.136.0beside@openai/codex@0.136.0in the codex runner workspace. - Built a
CodexSdkClientadapter that maps EJClaw's existing app-server input shape into SDK input. - Added
CODEX_RUNTIME=sdkruntime selection while keeping app-server as the default. - Added
CODEX_RUNTIME_SDK_ROLES=owner,arbitercanary limiting so production can try SDK only on selected paired roles. - Fallbacks:
/compactandcodexGoalsstay on app-server because SDK lacks those APIs. - Wrote unit tests around SDK event reduction, thread-id propagation, runtime selection, and unsupported steering.
- Ran real smoke turns through both the SDK adapter and the integrated codex-runner entrypoint against temporary git repos.
Evidence
Commands run:
bunx vitest run runners/codex-runner/test/sdk-client.test.ts
bunx vitest run runners/codex-runner/test/runtime-mode.test.ts
bunx tsc --noEmit -p runners/codex-runner/tsconfig.json
bun run --cwd runners/codex-runner build
bunx vitest run \
runners/codex-runner/test/app-server-client.test.ts \
runners/codex-runner/test/app-server-state.test.ts \
runners/codex-runner/test/runtime-mode.test.ts \
runners/codex-runner/test/sdk-client.test.ts
bun run check
CODEX_RUNTIME=sdk CODEX_EFFORT=minimal node runners/codex-runner/dist/index.js < /tmp/input.json
Observed adapter smoke result:
{
"status": "completed",
"result": "SDK_OK",
"error": null
}
Observed integrated runner smoke result:
{
"status": "success",
"result": "SDK_RUNNER_OK",
"phase": "final",
"newSessionId": "019e..."
}
What worked
- SDK import and runner workspace build work on Node/Bun in this repo.
- SDK streaming events can be reduced into EJClaw-style
status,threadId,result,error, andusage. thread.startedgives the real thread id during the first streamed turn, so a pending local handle can be replaced with the real session id.AbortControllergives a clean replacement for close-sentinel cancellation.- Real SDK smoke and integrated runner smoke completed successfully.
- Runtime flag selection works: default app-server;
CODEX_RUNTIME=sdkselects SDK;CODEX_RUNTIME_SDK_ROLEScanaries SDK to chosen paired roles;/compactand goals fall back to app-server.
What did not work / gaps
- SDK does not expose app-server
turn/steer; mid-turn Discord follow-up steering would need to be queued for the next turn or stay on the app-server path. - SDK TypeScript wrapper uses
codex exec --experimental-json, not persistentcodex app-server, so process-per-turn overhead and session behavior must be measured under real long tasks. - Passing
modelReasoningEffort: "minimal"failed in live smoke because Codex exec still enabled tools that are incompatible with minimal reasoning. The spike adapter coercesminimaltolow. - Existing Codex app-server-only features (
thread/goal/*,thread/compact/start) are not available through the current SDK API.
Verdict: FEATURE-FLAGGED
SDK is now viable as an optional exec-backed Codex runner lane behind CODEX_RUNTIME=sdk. The default remains app-server, and CODEX_RUNTIME_SDK_ROLES=owner,arbiter can limit SDK to safer paired roles while reviewer/non-paired turns keep the old path.
Recommended rollout
- Merge as a draft/feature-flagged implementation without changing production env.
- Enable
CODEX_RUNTIME=sdkfor one low-risk owner/arbiter lane first. - Keep app-server mode for rooms/tasks that require live steering, goals, or compaction.
- Watch
turn.failed,error,item.completed:error, empty final output, and process-per-turn latency before promoting SDK broadly.