fix: honor handoff agent type during failover
This commit is contained in:
@@ -452,6 +452,68 @@ describe('runAgentForGroup room memory', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('honors a forced agent type for reviewer failover handoffs', async () => {
|
||||
const group: RegisteredGroup = {
|
||||
...makeGroup(),
|
||||
agentType: 'codex',
|
||||
folder: 'test-group',
|
||||
};
|
||||
vi.mocked(serviceRouting.getEffectiveChannelLease).mockReturnValue({
|
||||
chat_jid: 'group@test',
|
||||
owner_service_id: 'codex-main',
|
||||
reviewer_service_id: 'claude',
|
||||
arbiter_service_id: null,
|
||||
activated_at: null,
|
||||
reason: null,
|
||||
explicit: false,
|
||||
});
|
||||
vi.mocked(db.getLatestOpenPairedTaskForChat).mockReturnValue({
|
||||
id: 'paired-task-reviewer-failover',
|
||||
chat_jid: 'group@test',
|
||||
group_folder: 'test-group',
|
||||
owner_service_id: 'codex-main',
|
||||
reviewer_service_id: 'claude',
|
||||
title: null,
|
||||
source_ref: 'HEAD',
|
||||
plan_notes: null,
|
||||
round_trip_count: 0,
|
||||
review_requested_at: '2026-03-31T00:00:00.000Z',
|
||||
status: 'active',
|
||||
arbiter_verdict: null,
|
||||
arbiter_requested_at: null,
|
||||
completion_reason: null,
|
||||
created_at: '2026-03-31T00:00:00.000Z',
|
||||
updated_at: '2026-03-31T00:00:00.000Z',
|
||||
});
|
||||
|
||||
await runAgentForGroup(
|
||||
{
|
||||
...makeDeps(),
|
||||
getSessions: () => ({ 'test-group:reviewer': 'claude-review-session' }),
|
||||
},
|
||||
{
|
||||
group,
|
||||
prompt: 'please retry review with codex',
|
||||
chatJid: 'group@test',
|
||||
runId: 'run-forced-reviewer-codex',
|
||||
forcedRole: 'reviewer',
|
||||
forcedAgentType: 'codex',
|
||||
},
|
||||
);
|
||||
|
||||
expect(agentRunner.runAgentProcess).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agentType: 'codex',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
sessionId: undefined,
|
||||
}),
|
||||
expect.any(Function),
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('allows silent reviewer outputs', async () => {
|
||||
const group = { ...makeGroup(), folder: 'test-group', workDir: '/repo' };
|
||||
vi.mocked(serviceRouting.getEffectiveChannelLease).mockReturnValue({
|
||||
|
||||
@@ -44,7 +44,9 @@ import {
|
||||
shouldRetryFreshSessionOnAgentFailure,
|
||||
} from './session-recovery.js';
|
||||
import {
|
||||
ARBITER_AGENT_TYPE,
|
||||
CODEX_REVIEW_SERVICE_ID,
|
||||
REVIEWER_AGENT_TYPE,
|
||||
SERVICE_SESSION_SCOPE,
|
||||
TIMEZONE,
|
||||
isClaudeService,
|
||||
@@ -69,7 +71,7 @@ import {
|
||||
} from './codex-token-rotation.js';
|
||||
import type { CodexRotationReason } from './agent-error-detection.js';
|
||||
import { getTokenCount } from './token-rotation.js';
|
||||
import type { PairedRoomRole, RegisteredGroup } from './types.js';
|
||||
import type { AgentType, PairedRoomRole, RegisteredGroup } from './types.js';
|
||||
|
||||
// ── Main executor ─────────────────────────────────────────────────
|
||||
|
||||
@@ -93,6 +95,7 @@ export async function runAgentForGroup(
|
||||
endSeq?: number | null;
|
||||
hasHumanMessage?: boolean;
|
||||
forcedRole?: PairedRoomRole;
|
||||
forcedAgentType?: AgentType;
|
||||
onOutput?: (output: AgentOutput) => Promise<void>;
|
||||
},
|
||||
): Promise<'success' | 'error'> {
|
||||
@@ -128,10 +131,11 @@ export async function runAgentForGroup(
|
||||
group.agentType,
|
||||
);
|
||||
|
||||
const effectiveAgentType = resolveEffectiveAgentType(
|
||||
const configuredAgentType = resolveEffectiveAgentType(
|
||||
activeRole,
|
||||
group.agentType,
|
||||
);
|
||||
const effectiveAgentType = args.forcedAgentType ?? configuredAgentType;
|
||||
const effectiveGroup =
|
||||
effectiveAgentType !== roleAgentPlan.ownerAgentType
|
||||
? { ...group, agentType: effectiveAgentType }
|
||||
@@ -144,7 +148,9 @@ export async function runAgentForGroup(
|
||||
);
|
||||
// Arbiter always starts fresh — never resume a previous session
|
||||
const sessionId =
|
||||
activeRole === 'arbiter' ? undefined : sessions[sessionFolder];
|
||||
activeRole === 'arbiter' || args.forcedAgentType
|
||||
? undefined
|
||||
: sessions[sessionFolder];
|
||||
const memoryBriefing = sessionId
|
||||
? undefined
|
||||
: await buildRoomMemoryBriefing({
|
||||
@@ -210,6 +216,29 @@ export async function runAgentForGroup(
|
||||
role: activeRole,
|
||||
serviceId: effectiveServiceId,
|
||||
});
|
||||
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: currentLease.reviewer_service_id,
|
||||
arbiterServiceId: currentLease.arbiter_service_id,
|
||||
reviewerMode,
|
||||
arbiterMode,
|
||||
sessionFolder,
|
||||
resumedSession: sessionId ?? null,
|
||||
},
|
||||
'Resolved execution target for agent turn',
|
||||
);
|
||||
|
||||
// ── MoA prompt enrichment ─────────────────────────────────────
|
||||
// When MoA is enabled and we're in arbiter mode, query external API
|
||||
|
||||
@@ -58,6 +58,7 @@ import {
|
||||
import {
|
||||
Channel,
|
||||
NewMessage,
|
||||
type AgentType,
|
||||
type PairedRoomRole,
|
||||
RegisteredGroup,
|
||||
type PairedTask,
|
||||
@@ -432,6 +433,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
||||
endSeq?: number | null;
|
||||
hasHumanMessage?: boolean;
|
||||
forcedRole?: PairedRoomRole;
|
||||
forcedAgentType?: AgentType;
|
||||
},
|
||||
): Promise<'success' | 'error'> =>
|
||||
runAgentForGroup(
|
||||
@@ -452,6 +454,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
||||
endSeq: options?.endSeq,
|
||||
hasHumanMessage: options?.hasHumanMessage,
|
||||
forcedRole: options?.forcedRole,
|
||||
forcedAgentType: options?.forcedAgentType,
|
||||
onOutput,
|
||||
},
|
||||
);
|
||||
@@ -466,6 +469,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
||||
endSeq: number | null;
|
||||
hasHumanMessage?: boolean;
|
||||
forcedRole?: PairedRoomRole;
|
||||
forcedAgentType?: AgentType;
|
||||
}): Promise<{
|
||||
outputStatus: 'success' | 'error';
|
||||
deliverySucceeded: boolean;
|
||||
@@ -479,7 +483,8 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
||||
}> => {
|
||||
const { group, prompt, chatJid, runId, channel, startSeq, endSeq } = args;
|
||||
const isClaudeCodeAgent =
|
||||
(group.agentType || 'claude-code') === 'claude-code';
|
||||
(args.forcedAgentType ?? group.agentType ?? 'claude-code') ===
|
||||
'claude-code';
|
||||
|
||||
const turnController = new MessageTurnController({
|
||||
chatJid,
|
||||
@@ -497,7 +502,8 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
||||
const workItem = createProducedWorkItem({
|
||||
group_folder: group.folder,
|
||||
chat_jid: chatJid,
|
||||
agent_type: group.agentType || 'claude-code',
|
||||
agent_type:
|
||||
args.forcedAgentType ?? group.agentType ?? 'claude-code',
|
||||
start_seq: startSeq,
|
||||
end_seq: endSeq,
|
||||
result_payload: text,
|
||||
@@ -527,6 +533,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
||||
endSeq,
|
||||
hasHumanMessage: args.hasHumanMessage,
|
||||
forcedRole: args.forcedRole,
|
||||
forcedAgentType: args.forcedAgentType,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -602,7 +609,9 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
||||
let handoffChannel = channel;
|
||||
if (handoffRole === 'reviewer') {
|
||||
const revChName =
|
||||
REVIEWER_AGENT_TYPE === 'claude-code' ? 'discord' : 'discord-review';
|
||||
handoff.target_agent_type === 'claude-code'
|
||||
? 'discord'
|
||||
: 'discord-review';
|
||||
handoffChannel = findChannelByName(deps.channels, revChName) || channel;
|
||||
} else if (handoffRole === 'arbiter') {
|
||||
handoffChannel =
|
||||
@@ -611,6 +620,20 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
||||
|
||||
const runId = `handoff-${handoff.id}`;
|
||||
try {
|
||||
logger.info(
|
||||
{
|
||||
chatJid: handoff.chat_jid,
|
||||
handoffId: handoff.id,
|
||||
runId,
|
||||
handoffRole,
|
||||
targetServiceId: handoff.target_service_id,
|
||||
targetAgentType: handoff.target_agent_type,
|
||||
reason: handoff.reason,
|
||||
intendedRole: handoff.intended_role ?? null,
|
||||
channelName: handoffChannel.name,
|
||||
},
|
||||
'Dispatching claimed service handoff',
|
||||
);
|
||||
const result = await executeTurn({
|
||||
group,
|
||||
prompt: handoff.prompt,
|
||||
@@ -620,6 +643,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
||||
startSeq: handoff.start_seq,
|
||||
endSeq: handoff.end_seq,
|
||||
forcedRole: handoffRole,
|
||||
forcedAgentType: handoff.target_agent_type,
|
||||
});
|
||||
|
||||
if (!result.deliverySucceeded) {
|
||||
|
||||
Reference in New Issue
Block a user