feat: implement MAGI 3-agent arbiter system for deadlock resolution
Introduces a third agent role (arbiter) that is summoned on-demand when owner and reviewer reach a deadlock (same verdict 3+ rounds). Architecture: - 3 Discord bots: owner (codex), reviewer (claude), arbiter (claude/codex) - Arbiter is NOT always-on — only invoked when deadlock detected - Arbiter renders binding verdict: PROCEED/REVISE/RESET/ESCALATE - Non-escalate verdicts reset round_trip_count and resume ping-pong - Backward compatible: ARBITER_AGENT_TYPE unset = existing 2-agent mode Changes across 13 source files + 7 test files: - types.ts: PairedRoomRole += 'arbiter', new statuses, ArbiterVerdict type - config.ts: ARBITER_AGENT_TYPE, ARBITER_SERVICE_ID, ARBITER_DEADLOCK_THRESHOLD - db.ts: schema migration (arbiter columns in channel_owner + paired_tasks) - service-routing.ts: arbiter_service_id in lease - room-role-context.ts: arbiter role detection - paired-execution-context.ts: deadlock->arbiter, verdict handling - message-runtime.ts: arbiter turn routing, cursor, sender labeling - message-agent-executor.ts: arbiter mode, failover - agent-runner.ts + environment.ts: arbiter container mode - platform-prompts.ts: arbiter prompt loading New files: - prompts/arbiter-paired-room.md: arbiter system prompt - src/arbiter-context.ts: builds conversation context for arbiter judgment
This commit is contained in:
@@ -34,6 +34,7 @@ import {
|
||||
shouldRetryFreshSessionOnAgentFailure,
|
||||
} from './session-recovery.js';
|
||||
import {
|
||||
ARBITER_AGENT_TYPE,
|
||||
CODEX_MAIN_SERVICE_ID,
|
||||
CODEX_REVIEW_SERVICE_ID,
|
||||
SERVICE_SESSION_SCOPE,
|
||||
@@ -96,29 +97,41 @@ export async function runAgentForGroup(
|
||||
: null;
|
||||
const effectiveServiceId =
|
||||
pairedTask &&
|
||||
(pairedTask.status === 'review_ready' || pairedTask.status === 'in_review')
|
||||
? currentLease.reviewer_service_id!
|
||||
: currentLease.owner_service_id;
|
||||
(pairedTask.status === 'arbiter_requested' || pairedTask.status === 'in_arbitration')
|
||||
? currentLease.arbiter_service_id!
|
||||
: pairedTask &&
|
||||
(pairedTask.status === 'review_ready' || pairedTask.status === 'in_review')
|
||||
? currentLease.reviewer_service_id!
|
||||
: currentLease.owner_service_id;
|
||||
const reviewerMode = effectiveServiceId === currentLease.reviewer_service_id;
|
||||
const arbiterMode = effectiveServiceId === currentLease.arbiter_service_id;
|
||||
|
||||
// Override agent type based on role config (OWNER_AGENT_TYPE / REVIEWER_AGENT_TYPE).
|
||||
// When the reviewer uses a different agent type than the group default,
|
||||
// swap in the configured type for the duration of this execution.
|
||||
const reviewerAgentTypeOverride =
|
||||
reviewerMode && REVIEWER_AGENT_TYPE !== (group.agentType || 'claude-code');
|
||||
const effectiveAgentType = reviewerAgentTypeOverride
|
||||
? REVIEWER_AGENT_TYPE
|
||||
: group.agentType || 'claude-code';
|
||||
const effectiveGroup = reviewerAgentTypeOverride
|
||||
? { ...group, agentType: REVIEWER_AGENT_TYPE }
|
||||
: group;
|
||||
const arbiterAgentTypeOverride =
|
||||
arbiterMode && ARBITER_AGENT_TYPE != null && ARBITER_AGENT_TYPE !== (group.agentType || 'claude-code');
|
||||
const effectiveAgentType = arbiterAgentTypeOverride
|
||||
? ARBITER_AGENT_TYPE!
|
||||
: reviewerAgentTypeOverride
|
||||
? REVIEWER_AGENT_TYPE
|
||||
: group.agentType || 'claude-code';
|
||||
const effectiveGroup = arbiterAgentTypeOverride
|
||||
? { ...group, agentType: ARBITER_AGENT_TYPE! }
|
||||
: reviewerAgentTypeOverride
|
||||
? { ...group, agentType: REVIEWER_AGENT_TYPE }
|
||||
: group;
|
||||
const isClaudeCodeAgent = effectiveAgentType === 'claude-code';
|
||||
|
||||
// When the reviewer uses a different agent type than the group default,
|
||||
// use a separate session key so owner and reviewer sessions don't collide.
|
||||
const sessionFolder = reviewerAgentTypeOverride
|
||||
? `${group.folder}:reviewer`
|
||||
: group.folder;
|
||||
// When the reviewer/arbiter uses a different agent type than the group default,
|
||||
// use a separate session key so owner, reviewer, and arbiter sessions don't collide.
|
||||
const sessionFolder = arbiterAgentTypeOverride
|
||||
? `${group.folder}:arbiter`
|
||||
: reviewerAgentTypeOverride
|
||||
? `${group.folder}:reviewer`
|
||||
: group.folder;
|
||||
const sessionId = sessions[sessionFolder];
|
||||
const memoryBriefing = sessionId
|
||||
? undefined
|
||||
@@ -194,6 +207,26 @@ export async function runAgentForGroup(
|
||||
return false;
|
||||
}
|
||||
|
||||
if (arbiterMode) {
|
||||
// Arbiter failed (e.g. Claude 401/429) — re-trigger arbitration with codex
|
||||
createServiceHandoff({
|
||||
chat_jid: chatJid,
|
||||
group_folder: group.folder,
|
||||
source_service_id: SERVICE_SESSION_SCOPE,
|
||||
target_service_id: CODEX_REVIEW_SERVICE_ID,
|
||||
target_agent_type: 'codex',
|
||||
prompt,
|
||||
start_seq: startSeq ?? null,
|
||||
end_seq: endSeq ?? null,
|
||||
reason: `arbiter-claude-${reason}`,
|
||||
});
|
||||
logger.warn(
|
||||
{ chatJid, group: group.name, runId, reason },
|
||||
'Claude arbiter unavailable, handed off arbiter turn to codex',
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (reviewerMode) {
|
||||
// Reviewer failed (e.g. Claude 401/429) — re-trigger review with codex
|
||||
// instead of swapping owner/reviewer roles.
|
||||
@@ -421,7 +454,9 @@ export async function runAgentForGroup(
|
||||
runId,
|
||||
provider: effectiveAgentType,
|
||||
reviewerMode,
|
||||
arbiterMode,
|
||||
reviewerAgentTypeOverride,
|
||||
arbiterAgentTypeOverride,
|
||||
},
|
||||
`Using provider: ${effectiveAgentType}`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user