Files
EJClaw/src/agent-runner.ts
Eyejoker f1679ac686 [codex] Remove Codex SDK canary runtime (#220)
* Revert "fix: drain sdk follow-up turns (#219)"

This reverts commit 807828e0ce.

* Revert "feat: feature-flag Codex SDK runner (#218)"

This reverts commit 879d16235f.
2026-06-03 19:10:53 +08:00

268 lines
7.5 KiB
TypeScript

import { ChildProcess, spawn } from 'child_process';
import fs from 'fs';
import path from 'path';
import {
EJCLAW_ENV,
isUnsafeHostPairedModeEnabled,
} from 'ejclaw-runners-shared';
/**
* Agent Process Runner for EJClaw
* Spawns agent execution as direct host processes and handles IPC.
*/
import {
prepareReadonlySessionEnvironment,
prepareGroupEnvironment,
type PreparedCodexSessionAuth,
} from './agent-runner-environment.js';
import { syncCodexSessionAuthBack } from './codex-token-rotation.js';
import { runSpawnedAgentProcess } from './agent-runner-process.js';
import { getStoredRoomSkillOverrides } from './db.js';
export {
type AvailableGroup,
writeGroupsSnapshot,
writeTasksSnapshot,
} from './agent-runner-snapshot.js';
import { logger } from './logger.js';
import { RegisteredGroup, RoomRoleContext } from './types.js';
import type { StoredRoomSkillOverride } from './db/rooms.js';
export interface AgentInput {
prompt: string;
sessionId?: string;
memoryBriefing?: string;
groupFolder: string;
chatJid: string;
runId?: string;
isMain: boolean;
isScheduledTask?: boolean;
runtimeTaskId?: string;
useTaskScopedSession?: boolean;
assistantName?: string;
agentType?: 'claude-code' | 'codex';
codexGoals?: boolean;
roomRoleContext?: RoomRoleContext;
}
export interface AgentOutput {
status: 'success' | 'error';
result: string | null;
output?: import('./types.js').StructuredAgentOutput;
phase?: import('./types.js').AgentOutputPhase;
agentId?: string;
agentLabel?: string;
agentDone?: boolean;
newSessionId?: string;
error?: string;
compaction?: {
completed: boolean;
trigger?: string | null;
};
}
function readRoomSkillOverridesForRunner(
chatJid: string,
): StoredRoomSkillOverride[] {
try {
return getStoredRoomSkillOverrides(chatJid);
} catch (err) {
logger.warn(
{
err,
chatJid,
},
'Failed to read room skill overrides; falling back to default skills',
);
return [];
}
}
function releaseCodexAuthSession(
auth: PreparedCodexSessionAuth | null | undefined,
): void {
auth?.lease?.release();
}
function finalizeCodexAuthSession(
auth: PreparedCodexSessionAuth | null | undefined,
): void {
if (!auth) return;
try {
syncCodexSessionAuthBack({
canonicalAuthPath: auth.canonicalAuthPath,
sessionAuthPath: auth.sessionAuthPath,
accountIndex: auth.accountIndex,
});
} catch (err) {
logger.warn(
{
err,
canonicalAuthPath: auth.canonicalAuthPath,
accountIndex: auth.accountIndex,
},
'Failed to sync Codex session auth back to canonical slot',
);
} finally {
auth.lease?.release();
}
}
function createCodexAuthSessionFinalizer(
getAuth: () => PreparedCodexSessionAuth | null | undefined,
): () => void {
let finalized = false;
return () => {
if (finalized) return;
finalized = true;
finalizeCodexAuthSession(getAuth());
};
}
export async function runAgentProcess(
group: RegisteredGroup,
input: AgentInput,
onProcess: (
proc: ChildProcess,
processName: string,
runtimeIpcDir: string,
) => void,
onOutput?: (output: AgentOutput) => Promise<void>,
envOverrides?: Record<string, string>,
): Promise<AgentOutput> {
const unsafeHostPairedMode = isUnsafeHostPairedModeEnabled(envOverrides);
// ── Host process mode (owner) ───────────────────────────────────
const startTime = Date.now();
const skillOverrides = readRoomSkillOverridesForRunner(input.chatJid);
const prepared = prepareGroupEnvironment(group, input.isMain, input.chatJid, {
memoryBriefing: input.memoryBriefing,
runtimeTaskId: input.runtimeTaskId,
useTaskScopedSession: input.useTaskScopedSession,
skillOverrides,
roomRole: input.roomRoleContext?.role,
});
const { env, groupDir, runnerDir } = prepared;
let codexSessionAuth = prepared.codexSessionAuth;
// Apply env overrides (caller-provided)
if (envOverrides) {
for (const [key, value] of Object.entries(envOverrides)) {
if (value) env[key] = value;
}
}
if (
unsafeHostPairedMode &&
envOverrides?.CLAUDE_CONFIG_DIR &&
(input.roomRoleContext?.role === 'reviewer' ||
input.roomRoleContext?.role === 'arbiter')
) {
const readonlySession = prepareReadonlySessionEnvironment({
sessionDir: envOverrides.CLAUDE_CONFIG_DIR,
chatJid: input.chatJid,
isMain: input.isMain,
groupFolder: group.folder,
agentType: group.agentType || 'claude-code',
memoryBriefing: input.memoryBriefing,
role: input.roomRoleContext.role,
ipcDir: env[EJCLAW_ENV.ipcDir],
hostIpcDir: env[EJCLAW_ENV.hostIpcDir],
workDir: envOverrides[EJCLAW_ENV.workDir] || env[EJCLAW_ENV.workDir],
skillOverrides,
});
if ((group.agentType || 'claude-code') === 'codex') {
releaseCodexAuthSession(codexSessionAuth);
codexSessionAuth = readonlySession.codexSessionAuth;
env.CODEX_HOME = path.join(envOverrides.CLAUDE_CONFIG_DIR, '.codex');
if (readonlySession.codexHomeDir) {
env.HOME = readonlySession.codexHomeDir;
}
}
}
if (input.runId) {
env[EJCLAW_ENV.runId] = input.runId;
}
const safeName = group.folder.replace(/[^a-zA-Z0-9-]/g, '-');
const processSuffix = input.runId || `${Date.now()}`;
const processName = `ejclaw-${safeName}-${processSuffix}`;
const finalizeCodexAuthSessionOnce = createCodexAuthSessionFinalizer(
() => codexSessionAuth,
);
// Check if runner is built
const distEntry = path.join(runnerDir, 'dist', 'index.js');
if (!fs.existsSync(distEntry)) {
logger.error(
{ runnerDir, chatJid: input.chatJid, runId: input.runId },
'Runner not built. Run: cd runners/agent-runner && bun install && bun run build',
);
releaseCodexAuthSession(codexSessionAuth);
return {
status: 'error',
result: null,
error: `Runner not built at ${distEntry}. Run bun run build:runners first.`,
};
}
logger.info(
{
group: group.name,
chatJid: input.chatJid,
groupFolder: input.groupFolder,
runId: input.runId,
processName,
agentType: group.agentType || 'claude-code',
isMain: input.isMain,
},
'Spawning agent process',
);
const logsDir = path.join(groupDir, 'logs');
fs.mkdirSync(logsDir, { recursive: true });
return new Promise((resolve) => {
const proc = spawn('bun', [distEntry], {
stdio: ['pipe', 'pipe', 'pipe'],
cwd: runnerDir,
env,
});
onProcess(proc, processName, env[EJCLAW_ENV.ipcDir]);
const runnerInput: AgentInput = {
...input,
...(group.agentConfig?.codexGoals === true ? { codexGoals: true } : {}),
};
proc.stdin.write(JSON.stringify(runnerInput));
proc.stdin.end();
runSpawnedAgentProcess({
proc,
group,
input: runnerInput,
processName,
logsDir,
startTime,
onOutput,
onTerminalStreamedOutputFlushed: finalizeCodexAuthSessionOnce,
})
.then((output) => {
finalizeCodexAuthSessionOnce();
resolve(output);
})
.catch((err: unknown) => {
finalizeCodexAuthSessionOnce();
logger.error(
{ err, processName, chatJid: input.chatJid, runId: input.runId },
'Spawned agent process runner failed',
);
resolve({
status: 'error',
result: null,
error: err instanceof Error ? err.message : String(err),
});
});
});
}