Files
EJClaw/src/message-agent-executor-target.ts

351 lines
11 KiB
TypeScript

import fs from 'fs';
import path from 'path';
import { listAvailableGroups } from './available-groups.js';
import {
ARBITER_AGENT_TYPE,
REVIEWER_AGENT_TYPE,
getRoleModelConfig,
} from './config.js';
import {
getAllTasks,
getLatestOpenPairedTaskForChat,
markPairedTurnRunning,
} from './db.js';
import { createScopedLogger } from './logger.js';
import { buildRoomMemoryBriefing } from './sqlite-memory-store.js';
import type { PreparedPairedExecutionContext } from './paired-execution-context.js';
import { preparePairedExecutionContext } from './paired-execution-context.js';
import {
resolveRuntimePairedTurnIdentity,
type PairedTurnIdentity,
} from './paired-turn-identity.js';
import { resolveExecutionTarget } from './message-runtime-rules.js';
import { buildRoomRoleContext } from './room-role-context.js';
import { getEffectiveChannelLease } from './service-routing.js';
import { getTokenCount } from './token-rotation.js';
import { writeGroupsSnapshot, writeTasksSnapshot } from './agent-runner.js';
import type {
AgentType,
PairedRoomRole,
RegisteredGroup,
RoomRoleContext,
} from './types.js';
interface ExecutionTargetDeps {
assistantName: string;
getRoomBindings: () => Record<string, RegisteredGroup>;
getSessions: () => Record<string, string>;
clearSession: (groupFolder: string) => void;
}
interface ExecutionTargetArgs {
group: RegisteredGroup;
prompt: string;
chatJid: string;
runId: string;
startSeq?: number | null;
endSeq?: number | null;
hasHumanMessage?: boolean;
forcedRole?: PairedRoomRole;
forcedAgentType?: AgentType;
pairedTurnIdentity?: PairedTurnIdentity;
}
export interface PreparedMessageAgentExecution {
currentLease: ReturnType<typeof getEffectiveChannelLease>;
pairedTask: ReturnType<typeof getLatestOpenPairedTaskForChat> | null;
activeRole: PairedRoomRole;
effectiveServiceId: string;
effectiveAgentType: AgentType;
sessionFolder: string;
reviewerMode: boolean;
arbiterMode: boolean;
effectiveGroup: RegisteredGroup;
isClaudeCodeAgent: boolean;
forceFreshClaudeReviewerSession: boolean;
shouldPersistSession: boolean;
currentSessionId: string | undefined;
provider: 'claude' | 'codex';
memoryBriefing: string | undefined;
canRetryClaudeCredentials: boolean;
roomRoleContext: RoomRoleContext | undefined;
pairedExecutionContext: PreparedPairedExecutionContext | undefined;
runtimePairedTurnIdentity: PairedTurnIdentity | undefined;
log: ReturnType<typeof createScopedLogger>;
clearRoleSdkSessions: () => void;
}
export async function prepareMessageAgentExecutionTarget(
deps: ExecutionTargetDeps,
args: ExecutionTargetArgs,
): Promise<PreparedMessageAgentExecution> {
const { group, chatJid, runId, startSeq, endSeq } = args;
const isMain = group.isMain === true;
const sessions = deps.getSessions();
const currentLease = getEffectiveChannelLease(chatJid);
const pairedTask = currentLease.reviewer_agent_type
? getLatestOpenPairedTaskForChat(chatJid)
: null;
const executionTarget = resolveExecutionTarget({
lease: currentLease,
pairedTaskStatus: pairedTask?.status,
groupFolder: group.folder,
groupAgentType: group.agentType,
forcedRole: args.forcedRole,
forcedAgentType: args.forcedAgentType,
});
const {
inferredRole,
canHonorForcedRole,
activeRole,
effectiveServiceId,
reviewerServiceId,
arbiterServiceId,
roleAgentPlan,
configuredAgentType,
effectiveAgentType,
sessionFolder,
} = executionTarget;
const reviewerMode = activeRole === 'reviewer';
const arbiterMode = activeRole === 'arbiter';
const effectiveGroup =
effectiveAgentType !== roleAgentPlan.ownerAgentType
? { ...group, agentType: effectiveAgentType }
: group;
const isClaudeCodeAgent = effectiveAgentType === 'claude-code';
const unsafeHostPairedMode =
process.env.EJCLAW_UNSAFE_HOST_PAIRED_MODE === '1';
const forceFreshClaudeReviewerSession =
reviewerMode && isClaudeCodeAgent && unsafeHostPairedMode;
const shouldPersistSession =
activeRole !== 'arbiter' &&
!args.forcedAgentType &&
!forceFreshClaudeReviewerSession;
const storedSessionId = sessions[sessionFolder];
if (forceFreshClaudeReviewerSession && storedSessionId) {
deps.clearSession(sessionFolder);
}
const currentSessionId =
activeRole === 'arbiter' ||
args.forcedAgentType ||
forceFreshClaudeReviewerSession
? undefined
: storedSessionId;
const provider = isClaudeCodeAgent ? 'claude' : 'codex';
const memoryBriefing = currentSessionId
? undefined
: await buildRoomMemoryBriefing({
groupFolder: group.folder,
groupName: group.name,
}).catch(() => undefined);
const tasks = getAllTasks(roleAgentPlan.ownerAgentType);
writeTasksSnapshot(
group.folder,
isMain,
tasks.map((task) => ({
id: task.id,
groupFolder: task.group_folder,
prompt: task.prompt,
schedule_type: task.schedule_type,
schedule_value: task.schedule_value,
status: task.status,
next_run: task.next_run,
})),
);
writeGroupsSnapshot(
group.folder,
isMain,
listAvailableGroups(deps.getRoomBindings()),
);
const claudeTokenCount = isClaudeCodeAgent ? getTokenCount() : 0;
const canRetryClaudeCredentials = isClaudeCodeAgent && claudeTokenCount > 0;
const roomRoleContext = buildRoomRoleContext(
currentLease,
effectiveServiceId,
activeRole,
);
const pairedExecutionContext = preparePairedExecutionContext({
group,
chatJid,
runId,
roomRoleContext,
hasHumanMessage: args.hasHumanMessage,
pairedTurnIdentity: args.pairedTurnIdentity,
});
const preparedTurnTaskUpdatedAt = pairedExecutionContext
? (pairedExecutionContext.claimedTaskUpdatedAt ??
pairedExecutionContext.task.updated_at)
: undefined;
const runtimePairedTurnIdentity =
args.pairedTurnIdentity ??
(pairedExecutionContext
? resolveRuntimePairedTurnIdentity({
taskId: pairedExecutionContext.task.id,
taskUpdatedAt:
preparedTurnTaskUpdatedAt ?? pairedExecutionContext.task.updated_at,
role: activeRole,
taskStatus: pairedExecutionContext.task.status,
hasHumanMessage: args.hasHumanMessage,
})
: pairedTask
? resolveRuntimePairedTurnIdentity({
taskId: pairedTask.id,
taskUpdatedAt: pairedTask.updated_at,
role: activeRole,
taskStatus: pairedTask.status,
hasHumanMessage: args.hasHumanMessage,
})
: undefined);
if (runtimePairedTurnIdentity) {
if (runtimePairedTurnIdentity.role !== activeRole) {
throw new Error(
`Paired turn ${runtimePairedTurnIdentity.turnId} cannot execute as ${activeRole}`,
);
}
if (
pairedExecutionContext &&
runtimePairedTurnIdentity.taskId !== pairedExecutionContext.task.id
) {
throw new Error(
`Paired turn ${runtimePairedTurnIdentity.turnId} task_id does not match the prepared execution context`,
);
}
if (
pairedExecutionContext &&
runtimePairedTurnIdentity.taskUpdatedAt !== preparedTurnTaskUpdatedAt
) {
throw new Error(
`Paired turn ${runtimePairedTurnIdentity.turnId} task_updated_at does not match the prepared execution context`,
);
}
if (
!pairedExecutionContext &&
pairedTask &&
runtimePairedTurnIdentity.taskId !== pairedTask.id
) {
throw new Error(
`Paired turn ${runtimePairedTurnIdentity.turnId} task_id does not match the latest paired task`,
);
}
if (
!pairedExecutionContext &&
pairedTask &&
runtimePairedTurnIdentity.taskUpdatedAt !== pairedTask.updated_at
) {
throw new Error(
`Paired turn ${runtimePairedTurnIdentity.turnId} task_updated_at does not match the latest paired task`,
);
}
}
if (runtimePairedTurnIdentity) {
markPairedTurnRunning({
turnIdentity: runtimePairedTurnIdentity,
executorServiceId: effectiveServiceId,
executorAgentType: effectiveAgentType,
runId,
});
}
if (pairedExecutionContext && !args.forcedAgentType) {
const roleConfig = getRoleModelConfig(activeRole);
if (roleConfig.model) {
const modelKey = isClaudeCodeAgent ? 'CLAUDE_MODEL' : 'CODEX_MODEL';
pairedExecutionContext.envOverrides[modelKey] = roleConfig.model;
}
if (roleConfig.effort) {
const effortKey = isClaudeCodeAgent ? 'CLAUDE_EFFORT' : 'CODEX_EFFORT';
pairedExecutionContext.envOverrides[effortKey] = roleConfig.effort;
}
}
if (pairedExecutionContext && runtimePairedTurnIdentity) {
pairedExecutionContext.envOverrides.EJCLAW_PAIRED_TURN_ID =
runtimePairedTurnIdentity.turnId;
pairedExecutionContext.envOverrides.EJCLAW_PAIRED_TURN_ROLE =
runtimePairedTurnIdentity.role;
pairedExecutionContext.envOverrides.EJCLAW_PAIRED_TURN_INTENT =
runtimePairedTurnIdentity.intentKind;
pairedExecutionContext.envOverrides.EJCLAW_PAIRED_TASK_UPDATED_AT =
runtimePairedTurnIdentity.taskUpdatedAt;
}
const log = createScopedLogger({
chatJid,
groupName: group.name,
groupFolder: group.folder,
runId,
messageSeqStart: startSeq ?? undefined,
messageSeqEnd: endSeq ?? undefined,
role: activeRole,
serviceId: effectiveServiceId,
turnId: runtimePairedTurnIdentity?.turnId,
});
log.info(
{
forcedRole: args.forcedRole,
forcedAgentType: args.forcedAgentType ?? null,
inferredRole,
canHonorForcedRole,
pairedTaskId: pairedTask?.id,
pairedTaskStatus: pairedTask?.status,
configuredAgentType,
effectiveServiceId,
effectiveAgentType,
groupAgentType: group.agentType,
configuredReviewerAgentType: REVIEWER_AGENT_TYPE,
configuredArbiterAgentType: ARBITER_AGENT_TYPE,
reviewerServiceId,
arbiterServiceId,
reviewerAgentType: currentLease.reviewer_agent_type,
arbiterAgentType: currentLease.arbiter_agent_type,
reviewerMode,
arbiterMode,
sessionFolder,
resumedSession: currentSessionId ?? null,
},
'Resolved execution target for agent turn',
);
const clearRoleSdkSessions = (): void => {
const configDir = pairedExecutionContext?.envOverrides?.CLAUDE_CONFIG_DIR;
if (!configDir) return;
for (const sdkDir of ['.claude', '.codex']) {
const sessionsDir = path.join(configDir, sdkDir, 'sessions');
if (fs.existsSync(sessionsDir)) {
fs.rmSync(sessionsDir, { recursive: true, force: true });
log.info({ sessionsDir }, 'Cleared SDK sessions for fresh Claude turn');
}
}
};
return {
currentLease,
pairedTask,
activeRole,
effectiveServiceId,
effectiveAgentType,
sessionFolder,
reviewerMode,
arbiterMode,
effectiveGroup,
isClaudeCodeAgent,
forceFreshClaudeReviewerSession,
shouldPersistSession,
currentSessionId,
provider,
memoryBriefing,
canRetryClaudeCredentials,
roomRoleContext,
pairedExecutionContext,
runtimePairedTurnIdentity,
log,
clearRoleSdkSessions,
};
}