Finalize turn-role invariants and legacy migration cutover

This commit is contained in:
ejclaw
2026-04-10 11:44:34 +09:00
parent 7753ee46f0
commit c67bd1c622
45 changed files with 15014 additions and 1381 deletions

View File

@@ -22,6 +22,7 @@ import {
createServiceHandoff,
getAllTasks,
getLatestOpenPairedTaskForChat,
markPairedTurnRunning,
} from './db.js';
import { GroupQueue } from './group-queue.js';
import { createScopedLogger } from './logger.js';
@@ -29,6 +30,10 @@ import { buildRoomMemoryBriefing } from './sqlite-memory-store.js';
import { preparePairedExecutionContext } from './paired-execution-context.js';
import { resolveCodexFallbackHandoff } from './paired-turn-fallback.js';
import { createPairedExecutionLifecycle } from './message-agent-executor-paired.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 { type AgentTriggerReason } from './agent-error-detection.js';
@@ -82,6 +87,7 @@ export async function runAgentForGroup(
hasHumanMessage?: boolean;
forcedRole?: PairedRoomRole;
forcedAgentType?: AgentType;
pairedTurnIdentity?: PairedTurnIdentity;
onOutput?: (output: AgentOutput) => Promise<void>;
},
): Promise<'success' | 'error'> {
@@ -189,6 +195,75 @@ export async function runAgentForGroup(
roomRoleContext,
hasHumanMessage: args.hasHumanMessage,
});
const runtimePairedTurnIdentity =
args.pairedTurnIdentity ??
(pairedExecutionContext
? resolveRuntimePairedTurnIdentity({
taskId: pairedExecutionContext.task.id,
taskUpdatedAt: 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 !==
pairedExecutionContext.task.updated_at
) {
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,
});
}
// Forced fallbacks run under a different agent runtime, so keep the
// fallback session on its default model/effort unless explicitly configured
// for that runtime elsewhere.
@@ -203,6 +278,16 @@ export async function runAgentForGroup(
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,
@@ -213,6 +298,7 @@ export async function runAgentForGroup(
messageSeqEnd: endSeq ?? undefined,
role: activeRole,
serviceId: effectiveServiceId,
turnId: runtimePairedTurnIdentity?.turnId,
});
log.info(
{
@@ -306,6 +392,7 @@ export async function runAgentForGroup(
const effectivePrompt = moaEnrichedPrompt;
const pairedExecutionLifecycle = createPairedExecutionLifecycle({
pairedExecutionContext,
pairedTurnIdentity: runtimePairedTurnIdentity,
completedRole: roomRoleContext?.role ?? 'owner',
chatJid,
runId,
@@ -349,8 +436,14 @@ export async function runAgentForGroup(
createServiceHandoff({
chat_jid: chatJid,
group_folder: group.folder,
paired_task_id: runtimePairedTurnIdentity?.taskId,
paired_task_updated_at: runtimePairedTurnIdentity?.taskUpdatedAt,
turn_id: runtimePairedTurnIdentity?.turnId,
turn_intent_kind: runtimePairedTurnIdentity?.intentKind,
turn_role: runtimePairedTurnIdentity?.role,
...handoffResolution.plan.handoff,
});
pairedExecutionLifecycle.markDelegated();
log.warn({ reason }, handoffResolution.plan.logMessage);
return true;
};