Simplify paired turn routing and prompt builders
This commit is contained in:
@@ -141,6 +141,17 @@ describe('message-agent-executor-rules', () => {
|
|||||||
).toBe('generic');
|
).toBe('generic');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('skips generic requeue when reviewer success moved the task to merge_ready for inline finalize', () => {
|
||||||
|
expect(
|
||||||
|
resolvePairedFollowUpQueueAction({
|
||||||
|
completedRole: 'reviewer',
|
||||||
|
executionStatus: 'succeeded',
|
||||||
|
sawOutput: true,
|
||||||
|
taskStatus: 'merge_ready',
|
||||||
|
}),
|
||||||
|
).toBe('skip-inline-finalize');
|
||||||
|
});
|
||||||
|
|
||||||
it('does not request a generic follow-up after successful reviewer output moved the task back to active', () => {
|
it('does not request a generic follow-up after successful reviewer output moved the task back to active', () => {
|
||||||
expect(
|
expect(
|
||||||
resolvePairedFollowUpQueueAction({
|
resolvePairedFollowUpQueueAction({
|
||||||
@@ -151,4 +162,15 @@ describe('message-agent-executor-rules', () => {
|
|||||||
}),
|
}),
|
||||||
).toBe('none');
|
).toBe('none');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns none after successful output when no next-turn action is needed', () => {
|
||||||
|
expect(
|
||||||
|
resolvePairedFollowUpQueueAction({
|
||||||
|
completedRole: 'owner',
|
||||||
|
executionStatus: 'succeeded',
|
||||||
|
sawOutput: true,
|
||||||
|
taskStatus: 'active',
|
||||||
|
}),
|
||||||
|
).toBe('none');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
isCodexRotationReason,
|
isCodexRotationReason,
|
||||||
} from './agent-error-detection.js';
|
} from './agent-error-detection.js';
|
||||||
import { detectCodexRotationTrigger } from './codex-token-rotation.js';
|
import { detectCodexRotationTrigger } from './codex-token-rotation.js';
|
||||||
|
import { resolveNextTurnAction } from './message-runtime-rules.js';
|
||||||
import type { PairedRoomRole, PairedTaskStatus } from './types.js';
|
import type { PairedRoomRole, PairedTaskStatus } from './types.js';
|
||||||
import { getErrorMessage } from './utils.js';
|
import { getErrorMessage } from './utils.js';
|
||||||
|
|
||||||
@@ -123,26 +124,29 @@ export function resolvePairedFollowUpQueueAction(args: {
|
|||||||
sawOutput: boolean;
|
sawOutput: boolean;
|
||||||
taskStatus: PairedTaskStatus | null;
|
taskStatus: PairedTaskStatus | null;
|
||||||
}): PairedFollowUpQueueAction {
|
}): PairedFollowUpQueueAction {
|
||||||
|
const nextTurnAction = resolveNextTurnAction({
|
||||||
|
taskStatus: args.taskStatus,
|
||||||
|
lastTurnOutputRole:
|
||||||
|
args.executionStatus === 'succeeded' && args.sawOutput
|
||||||
|
? args.completedRole
|
||||||
|
: null,
|
||||||
|
});
|
||||||
|
|
||||||
if (args.executionStatus === 'succeeded' && args.sawOutput) {
|
if (args.executionStatus === 'succeeded' && args.sawOutput) {
|
||||||
if (
|
if (nextTurnAction.kind === 'reviewer-turn') {
|
||||||
args.completedRole === 'owner' &&
|
|
||||||
args.taskStatus === 'review_ready'
|
|
||||||
) {
|
|
||||||
return 'generic';
|
return 'generic';
|
||||||
}
|
}
|
||||||
|
|
||||||
return args.completedRole === 'reviewer' &&
|
return args.completedRole === 'reviewer' &&
|
||||||
args.taskStatus === 'merge_ready'
|
nextTurnAction.kind === 'finalize-owner-turn'
|
||||||
? 'skip-inline-finalize'
|
? 'skip-inline-finalize'
|
||||||
: 'none';
|
: 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldRequeuePendingPairedTurn =
|
const shouldRequeuePendingPairedTurn =
|
||||||
(args.completedRole === 'reviewer' || args.completedRole === 'arbiter') &&
|
(args.completedRole === 'reviewer' || args.completedRole === 'arbiter') &&
|
||||||
(args.taskStatus === 'review_ready' ||
|
(nextTurnAction.kind === 'reviewer-turn' ||
|
||||||
args.taskStatus === 'in_review' ||
|
nextTurnAction.kind === 'arbiter-turn' ||
|
||||||
args.taskStatus === 'arbiter_requested' ||
|
nextTurnAction.kind === 'finalize-owner-turn');
|
||||||
args.taskStatus === 'in_arbitration' ||
|
|
||||||
args.taskStatus === 'merge_ready');
|
|
||||||
return shouldRequeuePendingPairedTurn ? 'pending' : 'none';
|
return shouldRequeuePendingPairedTurn ? 'pending' : 'none';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,10 +32,7 @@ import {
|
|||||||
} from './paired-execution-context.js';
|
} from './paired-execution-context.js';
|
||||||
import { resolveCodexFallbackHandoff } from './paired-turn-fallback.js';
|
import { resolveCodexFallbackHandoff } from './paired-turn-fallback.js';
|
||||||
import {
|
import {
|
||||||
resolveActiveRole,
|
resolveExecutionTarget,
|
||||||
resolveConfiguredRoleAgentPlan,
|
|
||||||
resolveEffectiveAgentType,
|
|
||||||
resolveSessionFolder,
|
|
||||||
} from './message-runtime-rules.js';
|
} from './message-runtime-rules.js';
|
||||||
import {
|
import {
|
||||||
isRetryableClaudeSessionFailureAttempt,
|
isRetryableClaudeSessionFailureAttempt,
|
||||||
@@ -67,7 +64,6 @@ import { readArbiterPrompt } from './platform-prompts.js';
|
|||||||
import {
|
import {
|
||||||
activateCodexFailover,
|
activateCodexFailover,
|
||||||
getEffectiveChannelLease,
|
getEffectiveChannelLease,
|
||||||
resolveLeaseServiceId,
|
|
||||||
} from './service-routing.js';
|
} from './service-routing.js';
|
||||||
import {
|
import {
|
||||||
evaluateStreamedOutput,
|
evaluateStreamedOutput,
|
||||||
@@ -119,41 +115,33 @@ export async function runAgentForGroup(
|
|||||||
const pairedTask = currentLease.reviewer_agent_type
|
const pairedTask = currentLease.reviewer_agent_type
|
||||||
? getLatestOpenPairedTaskForChat(chatJid)
|
? getLatestOpenPairedTaskForChat(chatJid)
|
||||||
: null;
|
: null;
|
||||||
const inferredRole = resolveActiveRole(pairedTask?.status);
|
const executionTarget = resolveExecutionTarget({
|
||||||
const canHonorForcedRole = Boolean(
|
lease: currentLease,
|
||||||
args.forcedRole === 'owner' ||
|
pairedTaskStatus: pairedTask?.status,
|
||||||
(args.forcedRole === 'reviewer' && currentLease.reviewer_agent_type) ||
|
groupFolder: group.folder,
|
||||||
(args.forcedRole === 'arbiter' && currentLease.arbiter_agent_type),
|
groupAgentType: group.agentType,
|
||||||
);
|
forcedRole: args.forcedRole,
|
||||||
const activeRole = canHonorForcedRole ? args.forcedRole! : inferredRole;
|
forcedAgentType: args.forcedAgentType,
|
||||||
const effectiveServiceId = resolveLeaseServiceId(currentLease, activeRole);
|
});
|
||||||
if (!effectiveServiceId) {
|
const {
|
||||||
throw new Error(`Missing runtime service id for ${activeRole} lease`);
|
inferredRole,
|
||||||
}
|
canHonorForcedRole,
|
||||||
|
activeRole,
|
||||||
|
effectiveServiceId,
|
||||||
|
reviewerServiceId,
|
||||||
|
arbiterServiceId,
|
||||||
|
roleAgentPlan,
|
||||||
|
configuredAgentType,
|
||||||
|
effectiveAgentType,
|
||||||
|
sessionFolder,
|
||||||
|
} = executionTarget;
|
||||||
const reviewerMode = activeRole === 'reviewer';
|
const reviewerMode = activeRole === 'reviewer';
|
||||||
const arbiterMode = activeRole === 'arbiter';
|
const arbiterMode = activeRole === 'arbiter';
|
||||||
const reviewerServiceId = resolveLeaseServiceId(currentLease, 'reviewer');
|
|
||||||
const arbiterServiceId = resolveLeaseServiceId(currentLease, 'arbiter');
|
|
||||||
const roleAgentPlan = resolveConfiguredRoleAgentPlan(
|
|
||||||
currentLease.reviewer_agent_type != null,
|
|
||||||
group.agentType,
|
|
||||||
);
|
|
||||||
|
|
||||||
const configuredAgentType = resolveEffectiveAgentType(
|
|
||||||
activeRole,
|
|
||||||
group.agentType,
|
|
||||||
);
|
|
||||||
const effectiveAgentType = args.forcedAgentType ?? configuredAgentType;
|
|
||||||
const effectiveGroup =
|
const effectiveGroup =
|
||||||
effectiveAgentType !== roleAgentPlan.ownerAgentType
|
effectiveAgentType !== roleAgentPlan.ownerAgentType
|
||||||
? { ...group, agentType: effectiveAgentType }
|
? { ...group, agentType: effectiveAgentType }
|
||||||
: group;
|
: group;
|
||||||
const isClaudeCodeAgent = effectiveAgentType === 'claude-code';
|
const isClaudeCodeAgent = effectiveAgentType === 'claude-code';
|
||||||
const sessionFolder = resolveSessionFolder(
|
|
||||||
group.folder,
|
|
||||||
activeRole,
|
|
||||||
group.agentType,
|
|
||||||
);
|
|
||||||
const unsafeHostPairedMode =
|
const unsafeHostPairedMode =
|
||||||
process.env.EJCLAW_UNSAFE_HOST_PAIRED_MODE === '1';
|
process.env.EJCLAW_UNSAFE_HOST_PAIRED_MODE === '1';
|
||||||
const forceFreshClaudeReviewerSession =
|
const forceFreshClaudeReviewerSession =
|
||||||
|
|||||||
141
src/message-runtime-prompts.ts
Normal file
141
src/message-runtime-prompts.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import { buildArbiterContextPrompt } from './arbiter-context.js';
|
||||||
|
import { formatMessages } from './router.js';
|
||||||
|
import type { NewMessage, PairedTask, PairedTurnOutput } from './types.js';
|
||||||
|
|
||||||
|
function turnOutputsToMessages(
|
||||||
|
outputs: PairedTurnOutput[],
|
||||||
|
chatJid: string,
|
||||||
|
): NewMessage[] {
|
||||||
|
return outputs.map((turnOutput) => ({
|
||||||
|
id: `turn-${turnOutput.task_id}-${turnOutput.turn_number}`,
|
||||||
|
chat_jid: chatJid,
|
||||||
|
sender: turnOutput.role,
|
||||||
|
sender_name: turnOutput.role,
|
||||||
|
content: turnOutput.output_text,
|
||||||
|
timestamp: turnOutput.created_at,
|
||||||
|
is_bot_message: true as const,
|
||||||
|
is_from_me: false as const,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeHumanAndTurnOutputMessages(
|
||||||
|
chatJid: string,
|
||||||
|
humanMessages: NewMessage[],
|
||||||
|
turnOutputs: PairedTurnOutput[],
|
||||||
|
): NewMessage[] {
|
||||||
|
return [...humanMessages, ...turnOutputsToMessages(turnOutputs, chatJid)].sort(
|
||||||
|
(a, b) => a.timestamp.localeCompare(b.timestamp),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildPairedTurnPrompt(args: {
|
||||||
|
taskId: string;
|
||||||
|
chatJid: string;
|
||||||
|
timezone: string;
|
||||||
|
missedMessages: NewMessage[];
|
||||||
|
labeledFallbackMessages: NewMessage[];
|
||||||
|
turnOutputs: PairedTurnOutput[];
|
||||||
|
}): string {
|
||||||
|
if (args.turnOutputs.length === 0) {
|
||||||
|
return formatMessages(args.labeledFallbackMessages, args.timezone);
|
||||||
|
}
|
||||||
|
|
||||||
|
const humanMessages = args.missedMessages.filter((message) => !message.is_bot_message);
|
||||||
|
return formatMessages(
|
||||||
|
mergeHumanAndTurnOutputMessages(
|
||||||
|
args.chatJid,
|
||||||
|
humanMessages,
|
||||||
|
args.turnOutputs,
|
||||||
|
),
|
||||||
|
args.timezone,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildReviewerPendingPrompt(args: {
|
||||||
|
chatJid: string;
|
||||||
|
timezone: string;
|
||||||
|
turnOutputs: PairedTurnOutput[];
|
||||||
|
recentHumanMessages: NewMessage[];
|
||||||
|
lastHumanMessage: string | null | undefined;
|
||||||
|
}): string {
|
||||||
|
if (args.turnOutputs.length > 0) {
|
||||||
|
return formatMessages(
|
||||||
|
mergeHumanAndTurnOutputMessages(
|
||||||
|
args.chatJid,
|
||||||
|
args.recentHumanMessages,
|
||||||
|
args.turnOutputs,
|
||||||
|
),
|
||||||
|
args.timezone,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!args.lastHumanMessage) {
|
||||||
|
return 'Review the latest owner changes in the workspace.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `User request:\n---\n${args.lastHumanMessage}\n---\n\nReview the latest owner changes in the workspace.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildOwnerPendingPrompt(args: {
|
||||||
|
chatJid: string;
|
||||||
|
timezone: string;
|
||||||
|
turnOutputs: PairedTurnOutput[];
|
||||||
|
recentHumanMessages: NewMessage[];
|
||||||
|
lastHumanMessage: string | null | undefined;
|
||||||
|
}): string {
|
||||||
|
if (args.turnOutputs.length > 0) {
|
||||||
|
return formatMessages(
|
||||||
|
mergeHumanAndTurnOutputMessages(
|
||||||
|
args.chatJid,
|
||||||
|
args.recentHumanMessages,
|
||||||
|
args.turnOutputs,
|
||||||
|
),
|
||||||
|
args.timezone,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!args.lastHumanMessage) {
|
||||||
|
return 'Continue the owner turn using the latest reviewer or arbiter feedback.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `User request:\n---\n${args.lastHumanMessage}\n---\n\nContinue the owner turn using the latest reviewer or arbiter feedback.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildArbiterPromptForTask(args: {
|
||||||
|
task: PairedTask;
|
||||||
|
chatJid: string;
|
||||||
|
timezone: string;
|
||||||
|
turnOutputs: PairedTurnOutput[];
|
||||||
|
recentMessages: NewMessage[];
|
||||||
|
labeledRecentMessages: NewMessage[];
|
||||||
|
}): string {
|
||||||
|
const messages =
|
||||||
|
args.turnOutputs.length > 0
|
||||||
|
? mergeHumanAndTurnOutputMessages(
|
||||||
|
args.chatJid,
|
||||||
|
args.recentMessages.filter((message) => !message.is_bot_message),
|
||||||
|
args.turnOutputs,
|
||||||
|
)
|
||||||
|
: args.labeledRecentMessages;
|
||||||
|
|
||||||
|
return buildArbiterContextPrompt({
|
||||||
|
chatJid: args.chatJid,
|
||||||
|
taskId: args.task.id,
|
||||||
|
roundTripCount: args.task.round_trip_count,
|
||||||
|
timezone: args.timezone,
|
||||||
|
messages,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildFinalizePendingPrompt(args: {
|
||||||
|
turnOutputs: PairedTurnOutput[];
|
||||||
|
}): string {
|
||||||
|
const lastReviewerOutput = [...args.turnOutputs]
|
||||||
|
.reverse()
|
||||||
|
.find((output) => output.role === 'reviewer');
|
||||||
|
const reviewerSummary = lastReviewerOutput?.output_text
|
||||||
|
? `\n\nReviewer's final assessment:\n${lastReviewerOutput.output_text.slice(0, 2000)}`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return `The reviewer approved your work (DONE). Finalize and report the result.${reviewerSummary}`;
|
||||||
|
}
|
||||||
132
src/message-runtime-rules.test.ts
Normal file
132
src/message-runtime-rules.test.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
resolveExecutionTarget,
|
||||||
|
resolveNextTurnAction,
|
||||||
|
resolveSessionFolder,
|
||||||
|
} from './message-runtime-rules.js';
|
||||||
|
import {
|
||||||
|
resolveLeaseServiceId,
|
||||||
|
type EffectiveChannelLease,
|
||||||
|
} from './service-routing.js';
|
||||||
|
|
||||||
|
const baseLease: EffectiveChannelLease = {
|
||||||
|
chat_jid: 'chat-1',
|
||||||
|
owner_agent_type: 'claude-code',
|
||||||
|
reviewer_agent_type: 'claude-code',
|
||||||
|
arbiter_agent_type: 'codex',
|
||||||
|
owner_service_id: 'svc-owner',
|
||||||
|
reviewer_service_id: 'svc-reviewer',
|
||||||
|
arbiter_service_id: 'svc-arbiter',
|
||||||
|
owner_failover_active: false,
|
||||||
|
activated_at: null,
|
||||||
|
reason: null,
|
||||||
|
explicit: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('message-runtime-rules', () => {
|
||||||
|
it('maps review_ready to a reviewer turn', () => {
|
||||||
|
expect(
|
||||||
|
resolveNextTurnAction({
|
||||||
|
taskStatus: 'review_ready',
|
||||||
|
}),
|
||||||
|
).toEqual({ kind: 'reviewer-turn' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps arbiter_requested to an arbiter turn', () => {
|
||||||
|
expect(
|
||||||
|
resolveNextTurnAction({
|
||||||
|
taskStatus: 'arbiter_requested',
|
||||||
|
}),
|
||||||
|
).toEqual({ kind: 'arbiter-turn' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps merge_ready to an inline finalize owner turn', () => {
|
||||||
|
expect(
|
||||||
|
resolveNextTurnAction({
|
||||||
|
taskStatus: 'merge_ready',
|
||||||
|
}),
|
||||||
|
).toEqual({ kind: 'finalize-owner-turn' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps active tasks with reviewer output to an owner follow-up', () => {
|
||||||
|
expect(
|
||||||
|
resolveNextTurnAction({
|
||||||
|
taskStatus: 'active',
|
||||||
|
lastTurnOutputRole: 'reviewer',
|
||||||
|
}),
|
||||||
|
).toEqual({ kind: 'owner-follow-up' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns none when an active task has no reviewer or arbiter handoff output', () => {
|
||||||
|
expect(
|
||||||
|
resolveNextTurnAction({
|
||||||
|
taskStatus: 'active',
|
||||||
|
lastTurnOutputRole: 'owner',
|
||||||
|
}),
|
||||||
|
).toEqual({ kind: 'none' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves reviewer execution target from review_ready task status', () => {
|
||||||
|
const resolution = resolveExecutionTarget({
|
||||||
|
lease: baseLease,
|
||||||
|
pairedTaskStatus: 'review_ready',
|
||||||
|
groupFolder: 'group-1',
|
||||||
|
groupAgentType: 'claude-code',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resolution).toMatchObject({
|
||||||
|
inferredRole: 'reviewer',
|
||||||
|
activeRole: 'reviewer',
|
||||||
|
configuredAgentType: 'claude-code',
|
||||||
|
effectiveAgentType: 'claude-code',
|
||||||
|
sessionFolder: 'group-1',
|
||||||
|
});
|
||||||
|
expect(resolution.effectiveServiceId).toBe(resolution.reviewerServiceId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('honors an arbiter forced role only when arbiter is configured', () => {
|
||||||
|
const resolution = resolveExecutionTarget({
|
||||||
|
lease: { ...baseLease, arbiter_agent_type: null, arbiter_service_id: null },
|
||||||
|
pairedTaskStatus: 'active',
|
||||||
|
groupFolder: 'group-1',
|
||||||
|
groupAgentType: 'claude-code',
|
||||||
|
forcedRole: 'arbiter',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resolution).toMatchObject({
|
||||||
|
inferredRole: 'owner',
|
||||||
|
canHonorForcedRole: false,
|
||||||
|
activeRole: 'owner',
|
||||||
|
});
|
||||||
|
expect(resolution.effectiveServiceId).toBe(
|
||||||
|
resolveLeaseServiceId(baseLease, 'owner'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies forced agent type overrides without changing the configured role target', () => {
|
||||||
|
const resolution = resolveExecutionTarget({
|
||||||
|
lease: baseLease,
|
||||||
|
pairedTaskStatus: 'review_ready',
|
||||||
|
groupFolder: 'group-1',
|
||||||
|
groupAgentType: 'claude-code',
|
||||||
|
forcedAgentType: 'codex',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resolution).toMatchObject({
|
||||||
|
activeRole: 'reviewer',
|
||||||
|
configuredAgentType: 'claude-code',
|
||||||
|
effectiveAgentType: 'codex',
|
||||||
|
});
|
||||||
|
expect(resolution.effectiveServiceId).toBe(resolution.reviewerServiceId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('always gives arbiter a dedicated session folder', () => {
|
||||||
|
expect(
|
||||||
|
resolveSessionFolder('group-1', 'arbiter', 'claude-code'),
|
||||||
|
).toBe('group-1:arbiter');
|
||||||
|
expect(
|
||||||
|
resolveSessionFolder('group-1', 'arbiter', 'codex'),
|
||||||
|
).toBe('group-1:arbiter');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,7 +4,11 @@ import { normalizeStoredSeqCursor } from './message-cursor.js';
|
|||||||
import { isTriggerAllowed, loadSenderAllowlist } from './sender-allowlist.js';
|
import { isTriggerAllowed, loadSenderAllowlist } from './sender-allowlist.js';
|
||||||
import { isTaskStatusControlMessage } from './task-watch-status.js';
|
import { isTaskStatusControlMessage } from './task-watch-status.js';
|
||||||
import { ARBITER_AGENT_TYPE, REVIEWER_AGENT_TYPE } from './config.js';
|
import { ARBITER_AGENT_TYPE, REVIEWER_AGENT_TYPE } from './config.js';
|
||||||
import { hasReviewerLease } from './service-routing.js';
|
import {
|
||||||
|
hasReviewerLease,
|
||||||
|
resolveLeaseServiceId,
|
||||||
|
type EffectiveChannelLease,
|
||||||
|
} from './service-routing.js';
|
||||||
import {
|
import {
|
||||||
resolveAgentTypeForRole,
|
resolveAgentTypeForRole,
|
||||||
resolveRoleAgentPlan,
|
resolveRoleAgentPlan,
|
||||||
@@ -14,6 +18,8 @@ import {
|
|||||||
type AgentType,
|
type AgentType,
|
||||||
type Channel,
|
type Channel,
|
||||||
type NewMessage,
|
type NewMessage,
|
||||||
|
type PairedRoomRole,
|
||||||
|
type PairedTaskStatus,
|
||||||
type RegisteredGroup,
|
type RegisteredGroup,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
|
||||||
@@ -55,6 +61,36 @@ export function resolveActiveRole(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NextTurnAction =
|
||||||
|
| { kind: 'none' }
|
||||||
|
| { kind: 'reviewer-turn' }
|
||||||
|
| { kind: 'arbiter-turn' }
|
||||||
|
| { kind: 'owner-follow-up' }
|
||||||
|
| { kind: 'finalize-owner-turn' };
|
||||||
|
|
||||||
|
export function resolveNextTurnAction(args: {
|
||||||
|
taskStatus?: PairedTaskStatus | null;
|
||||||
|
lastTurnOutputRole?: PairedRoomRole | null;
|
||||||
|
}): NextTurnAction {
|
||||||
|
switch (args.taskStatus) {
|
||||||
|
case 'review_ready':
|
||||||
|
case 'in_review':
|
||||||
|
return { kind: 'reviewer-turn' };
|
||||||
|
case 'arbiter_requested':
|
||||||
|
case 'in_arbitration':
|
||||||
|
return { kind: 'arbiter-turn' };
|
||||||
|
case 'merge_ready':
|
||||||
|
return { kind: 'finalize-owner-turn' };
|
||||||
|
case 'active':
|
||||||
|
return args.lastTurnOutputRole === 'reviewer' ||
|
||||||
|
args.lastTurnOutputRole === 'arbiter'
|
||||||
|
? { kind: 'owner-follow-up' }
|
||||||
|
: { kind: 'none' };
|
||||||
|
default:
|
||||||
|
return { kind: 'none' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Cursor key for a role. Owner uses chatJid, others use chatJid:role. */
|
/** Cursor key for a role. Owner uses chatJid, others use chatJid:role. */
|
||||||
export function resolveCursorKey(
|
export function resolveCursorKey(
|
||||||
chatJid: string,
|
chatJid: string,
|
||||||
@@ -102,6 +138,69 @@ export function resolveSessionFolder(
|
|||||||
return `${groupFolder}:${role}`;
|
return `${groupFolder}:${role}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExecutionTargetResolution {
|
||||||
|
inferredRole: PairedRoomRole;
|
||||||
|
canHonorForcedRole: boolean;
|
||||||
|
activeRole: PairedRoomRole;
|
||||||
|
effectiveServiceId: string;
|
||||||
|
reviewerServiceId: string | null;
|
||||||
|
arbiterServiceId: string | null;
|
||||||
|
roleAgentPlan: RoleAgentPlan;
|
||||||
|
configuredAgentType: AgentType;
|
||||||
|
effectiveAgentType: AgentType;
|
||||||
|
sessionFolder: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveExecutionTarget(args: {
|
||||||
|
lease: EffectiveChannelLease;
|
||||||
|
pairedTaskStatus?: PairedTaskStatus | null;
|
||||||
|
groupFolder: string;
|
||||||
|
groupAgentType?: AgentType;
|
||||||
|
forcedRole?: PairedRoomRole;
|
||||||
|
forcedAgentType?: AgentType;
|
||||||
|
}): ExecutionTargetResolution {
|
||||||
|
const inferredRole = resolveActiveRole(args.pairedTaskStatus);
|
||||||
|
const canHonorForcedRole = Boolean(
|
||||||
|
args.forcedRole === 'owner' ||
|
||||||
|
(args.forcedRole === 'reviewer' && args.lease.reviewer_agent_type) ||
|
||||||
|
(args.forcedRole === 'arbiter' && args.lease.arbiter_agent_type),
|
||||||
|
);
|
||||||
|
const activeRole = canHonorForcedRole ? args.forcedRole! : inferredRole;
|
||||||
|
const effectiveServiceId = resolveLeaseServiceId(args.lease, activeRole);
|
||||||
|
if (!effectiveServiceId) {
|
||||||
|
throw new Error(`Missing runtime service id for ${activeRole} lease`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const reviewerServiceId = resolveLeaseServiceId(args.lease, 'reviewer');
|
||||||
|
const arbiterServiceId = resolveLeaseServiceId(args.lease, 'arbiter');
|
||||||
|
const roleAgentPlan = resolveConfiguredRoleAgentPlan(
|
||||||
|
args.lease.reviewer_agent_type != null,
|
||||||
|
args.groupAgentType,
|
||||||
|
);
|
||||||
|
const configuredAgentType = resolveEffectiveAgentType(
|
||||||
|
activeRole,
|
||||||
|
args.groupAgentType,
|
||||||
|
);
|
||||||
|
const effectiveAgentType = args.forcedAgentType ?? configuredAgentType;
|
||||||
|
|
||||||
|
return {
|
||||||
|
inferredRole,
|
||||||
|
canHonorForcedRole,
|
||||||
|
activeRole,
|
||||||
|
effectiveServiceId,
|
||||||
|
reviewerServiceId,
|
||||||
|
arbiterServiceId,
|
||||||
|
roleAgentPlan,
|
||||||
|
configuredAgentType,
|
||||||
|
effectiveAgentType,
|
||||||
|
sessionFolder: resolveSessionFolder(
|
||||||
|
args.groupFolder,
|
||||||
|
activeRole,
|
||||||
|
args.groupAgentType,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function createImplicitContinuationTracker(idleTimeout: number) {
|
export function createImplicitContinuationTracker(idleTimeout: number) {
|
||||||
const implicitContinuationUntil = new Map<string, number>();
|
const implicitContinuationUntil = new Map<string, number>();
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import { isTriggerAllowed, loadSenderAllowlist } from './sender-allowlist.js';
|
|||||||
import {
|
import {
|
||||||
advanceLastAgentCursor,
|
advanceLastAgentCursor,
|
||||||
createImplicitContinuationTracker,
|
createImplicitContinuationTracker,
|
||||||
|
resolveNextTurnAction,
|
||||||
resolveActiveRole,
|
resolveActiveRole,
|
||||||
resolveCursorKey,
|
resolveCursorKey,
|
||||||
filterLoopingPairedBotMessages,
|
filterLoopingPairedBotMessages,
|
||||||
@@ -40,9 +41,15 @@ import {
|
|||||||
hasAllowedTrigger,
|
hasAllowedTrigger,
|
||||||
shouldSkipBotOnlyCollaboration,
|
shouldSkipBotOnlyCollaboration,
|
||||||
} from './message-runtime-rules.js';
|
} from './message-runtime-rules.js';
|
||||||
import { buildArbiterContextPrompt } from './arbiter-context.js';
|
|
||||||
import { runAgentForGroup } from './message-agent-executor.js';
|
import { runAgentForGroup } from './message-agent-executor.js';
|
||||||
import { MessageTurnController } from './message-turn-controller.js';
|
import { MessageTurnController } from './message-turn-controller.js';
|
||||||
|
import {
|
||||||
|
buildArbiterPromptForTask,
|
||||||
|
buildFinalizePendingPrompt,
|
||||||
|
buildOwnerPendingPrompt,
|
||||||
|
buildPairedTurnPrompt,
|
||||||
|
buildReviewerPendingPrompt,
|
||||||
|
} from './message-runtime-prompts.js';
|
||||||
import {
|
import {
|
||||||
extractSessionCommand,
|
extractSessionCommand,
|
||||||
handleSessionCommand,
|
handleSessionCommand,
|
||||||
@@ -56,7 +63,6 @@ import {
|
|||||||
type PairedRoomRole,
|
type PairedRoomRole,
|
||||||
RegisteredGroup,
|
RegisteredGroup,
|
||||||
type PairedTask,
|
type PairedTask,
|
||||||
type PairedTurnOutput,
|
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import { createScopedLogger, logger } from './logger.js';
|
import { createScopedLogger, logger } from './logger.js';
|
||||||
import { resolveGroupIpcPath } from './group-folder.js';
|
import { resolveGroupIpcPath } from './group-folder.js';
|
||||||
@@ -193,144 +199,6 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Convert paired turn outputs to NewMessage format for formatMessages(). */
|
|
||||||
const turnOutputsToMessages = (
|
|
||||||
outputs: PairedTurnOutput[],
|
|
||||||
chatJid: string,
|
|
||||||
): NewMessage[] =>
|
|
||||||
outputs.map((t) => ({
|
|
||||||
id: `turn-${t.task_id}-${t.turn_number}`,
|
|
||||||
chat_jid: chatJid,
|
|
||||||
sender: t.role,
|
|
||||||
sender_name: t.role,
|
|
||||||
content: t.output_text,
|
|
||||||
timestamp: t.created_at,
|
|
||||||
is_bot_message: true as const,
|
|
||||||
is_from_me: false as const,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const mergeHumanAndTurnOutputMessages = (
|
|
||||||
chatJid: string,
|
|
||||||
humanMessages: NewMessage[],
|
|
||||||
turnOutputs: PairedTurnOutput[],
|
|
||||||
): NewMessage[] =>
|
|
||||||
[...humanMessages, ...turnOutputsToMessages(turnOutputs, chatJid)].sort(
|
|
||||||
(a, b) => a.timestamp.localeCompare(b.timestamp),
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build a prompt from paired_turn_outputs (Discord-independent) + human messages.
|
|
||||||
* Falls back to the legacy labelPairedSenders path when no turn outputs exist.
|
|
||||||
*/
|
|
||||||
const buildPairedTurnPrompt = (
|
|
||||||
taskId: string,
|
|
||||||
chatJid: string,
|
|
||||||
timezone: string,
|
|
||||||
missedMessages: NewMessage[],
|
|
||||||
): string => {
|
|
||||||
const turnOutputs = getPairedTurnOutputs(taskId);
|
|
||||||
if (turnOutputs.length === 0) {
|
|
||||||
// No stored outputs yet — fall back to Discord messages
|
|
||||||
return formatMessages(
|
|
||||||
labelPairedSenders(chatJid, missedMessages),
|
|
||||||
timezone,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Human messages from the missed messages (exclude bot messages)
|
|
||||||
const humanMessages = missedMessages.filter((m) => !m.is_bot_message);
|
|
||||||
|
|
||||||
return formatMessages(
|
|
||||||
mergeHumanAndTurnOutputMessages(chatJid, humanMessages, turnOutputs),
|
|
||||||
timezone,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildReviewerPendingPrompt = (
|
|
||||||
task: PairedTask,
|
|
||||||
chatJid: string,
|
|
||||||
timezone: string,
|
|
||||||
): string => {
|
|
||||||
const turnOutputs = getPairedTurnOutputs(task.id);
|
|
||||||
if (turnOutputs.length > 0) {
|
|
||||||
const humanMessages = getRecentChatMessages(chatJid, 20).filter(
|
|
||||||
(message) => !message.is_bot_message,
|
|
||||||
);
|
|
||||||
return formatMessages(
|
|
||||||
mergeHumanAndTurnOutputMessages(chatJid, humanMessages, turnOutputs),
|
|
||||||
timezone,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const userMessage = getLastHumanMessageContent(chatJid);
|
|
||||||
if (!userMessage) {
|
|
||||||
return 'Review the latest owner changes in the workspace.';
|
|
||||||
}
|
|
||||||
|
|
||||||
return `User request:\n---\n${userMessage}\n---\n\nReview the latest owner changes in the workspace.`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildOwnerPendingPrompt = (
|
|
||||||
task: PairedTask,
|
|
||||||
chatJid: string,
|
|
||||||
timezone: string,
|
|
||||||
): string => {
|
|
||||||
const turnOutputs = getPairedTurnOutputs(task.id);
|
|
||||||
if (turnOutputs.length > 0) {
|
|
||||||
const humanMessages = getRecentChatMessages(chatJid, 20).filter(
|
|
||||||
(message) => !message.is_bot_message,
|
|
||||||
);
|
|
||||||
return formatMessages(
|
|
||||||
mergeHumanAndTurnOutputMessages(chatJid, humanMessages, turnOutputs),
|
|
||||||
timezone,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const userMessage = getLastHumanMessageContent(chatJid);
|
|
||||||
if (!userMessage) {
|
|
||||||
return 'Continue the owner turn using the latest reviewer or arbiter feedback.';
|
|
||||||
}
|
|
||||||
|
|
||||||
return `User request:\n---\n${userMessage}\n---\n\nContinue the owner turn using the latest reviewer or arbiter feedback.`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildArbiterPromptForTask = (
|
|
||||||
task: PairedTask,
|
|
||||||
chatJid: string,
|
|
||||||
timezone: string,
|
|
||||||
): string => {
|
|
||||||
const turnOutputs = getPairedTurnOutputs(task.id);
|
|
||||||
const recentMessages = getRecentChatMessages(chatJid, 20);
|
|
||||||
const arbiterMessages =
|
|
||||||
turnOutputs.length > 0
|
|
||||||
? mergeHumanAndTurnOutputMessages(
|
|
||||||
chatJid,
|
|
||||||
recentMessages.filter((message) => !message.is_bot_message),
|
|
||||||
turnOutputs,
|
|
||||||
)
|
|
||||||
: labelPairedSenders(chatJid, recentMessages);
|
|
||||||
|
|
||||||
return buildArbiterContextPrompt({
|
|
||||||
chatJid,
|
|
||||||
taskId: task.id,
|
|
||||||
roundTripCount: task.round_trip_count,
|
|
||||||
timezone,
|
|
||||||
messages: arbiterMessages,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildFinalizePendingPrompt = (task: PairedTask): string => {
|
|
||||||
const turnOutputs = getPairedTurnOutputs(task.id);
|
|
||||||
const lastReviewerOutput = [...turnOutputs]
|
|
||||||
.reverse()
|
|
||||||
.find((output) => output.role === 'reviewer');
|
|
||||||
const reviewerSummary = lastReviewerOutput?.output_text
|
|
||||||
? `\n\nReviewer's final assessment:\n${lastReviewerOutput.output_text.slice(0, 2000)}`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
return `The reviewer approved your work (DONE). Finalize and report the result.${reviewerSummary}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isBotOnlyPairedRoomTurn = (
|
const isBotOnlyPairedRoomTurn = (
|
||||||
chatJid: string,
|
chatJid: string,
|
||||||
messages: NewMessage[],
|
messages: NewMessage[],
|
||||||
@@ -370,7 +238,11 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
const cursor =
|
const cursor =
|
||||||
pendingCursorSource?.seq ?? pendingCursorSource?.timestamp ?? null;
|
pendingCursorSource?.seq ?? pendingCursorSource?.timestamp ?? null;
|
||||||
|
|
||||||
if (task.status === 'merge_ready') {
|
const nextTurnAction = resolveNextTurnAction({
|
||||||
|
taskStatus: task.status,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (nextTurnAction.kind === 'finalize-owner-turn') {
|
||||||
return {
|
return {
|
||||||
kind: 'inline-finalize',
|
kind: 'inline-finalize',
|
||||||
task,
|
task,
|
||||||
@@ -379,18 +251,16 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
task.status === 'review_ready' ||
|
nextTurnAction.kind === 'reviewer-turn' ||
|
||||||
task.status === 'in_review' ||
|
nextTurnAction.kind === 'arbiter-turn'
|
||||||
task.status === 'arbiter_requested' ||
|
|
||||||
task.status === 'in_arbitration'
|
|
||||||
) {
|
) {
|
||||||
const nextRole = resolveActiveRole(task.status);
|
|
||||||
return {
|
return {
|
||||||
kind: 'requeue-pending-turn',
|
kind: 'requeue-pending-turn',
|
||||||
task,
|
task,
|
||||||
cursor,
|
cursor,
|
||||||
cursorKey: resolveCursorKey(chatJid, task.status),
|
cursorKey: resolveCursorKey(chatJid, task.status),
|
||||||
nextRole: nextRole === 'arbiter' ? 'arbiter' : 'reviewer',
|
nextRole:
|
||||||
|
nextTurnAction.kind === 'arbiter-turn' ? 'arbiter' : 'reviewer',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,7 +316,9 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
);
|
);
|
||||||
const { deliverySucceeded } = await executeTurn({
|
const { deliverySucceeded } = await executeTurn({
|
||||||
group,
|
group,
|
||||||
prompt: buildFinalizePendingPrompt(action.task),
|
prompt: buildFinalizePendingPrompt({
|
||||||
|
turnOutputs: getPairedTurnOutputs(action.task.id),
|
||||||
|
}),
|
||||||
chatJid,
|
chatJid,
|
||||||
runId,
|
runId,
|
||||||
channel,
|
channel,
|
||||||
@@ -966,11 +838,27 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
const lastRaw = rawMissedMessages[rawMissedMessages.length - 1];
|
const lastRaw = rawMissedMessages[rawMissedMessages.length - 1];
|
||||||
const cursor = lastRaw?.seq ?? lastRaw?.timestamp ?? null;
|
const cursor = lastRaw?.seq ?? lastRaw?.timestamp ?? null;
|
||||||
const taskStatus = task.status;
|
const taskStatus = task.status;
|
||||||
const pendingRole = resolveActiveRole(taskStatus);
|
const turnOutputs = getPairedTurnOutputs(task.id);
|
||||||
|
const lastTurnOutput = turnOutputs[turnOutputs.length - 1];
|
||||||
|
const nextTurnAction = resolveNextTurnAction({
|
||||||
|
taskStatus,
|
||||||
|
lastTurnOutputRole: lastTurnOutput?.role ?? null,
|
||||||
|
});
|
||||||
|
const recentMessages = getRecentChatMessages(chatJid, 20);
|
||||||
|
const recentHumanMessages = recentMessages.filter(
|
||||||
|
(message) => !message.is_bot_message,
|
||||||
|
);
|
||||||
|
const lastHumanMessage = getLastHumanMessageContent(chatJid);
|
||||||
|
|
||||||
if (pendingRole === 'reviewer') {
|
if (nextTurnAction.kind === 'reviewer-turn') {
|
||||||
return {
|
return {
|
||||||
prompt: buildReviewerPendingPrompt(task, chatJid, deps.timezone),
|
prompt: buildReviewerPendingPrompt({
|
||||||
|
chatJid,
|
||||||
|
timezone: deps.timezone,
|
||||||
|
turnOutputs,
|
||||||
|
recentHumanMessages,
|
||||||
|
lastHumanMessage,
|
||||||
|
}),
|
||||||
channel: resolveChannel(taskStatus),
|
channel: resolveChannel(taskStatus),
|
||||||
cursor,
|
cursor,
|
||||||
cursorKey: resolveCursorKey(chatJid, taskStatus),
|
cursorKey: resolveCursorKey(chatJid, taskStatus),
|
||||||
@@ -978,9 +866,16 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pendingRole === 'arbiter') {
|
if (nextTurnAction.kind === 'arbiter-turn') {
|
||||||
return {
|
return {
|
||||||
prompt: buildArbiterPromptForTask(task, chatJid, deps.timezone),
|
prompt: buildArbiterPromptForTask({
|
||||||
|
task,
|
||||||
|
chatJid,
|
||||||
|
timezone: deps.timezone,
|
||||||
|
turnOutputs,
|
||||||
|
recentMessages,
|
||||||
|
labeledRecentMessages: labelPairedSenders(chatJid, recentMessages),
|
||||||
|
}),
|
||||||
channel: resolveChannel(taskStatus),
|
channel: resolveChannel(taskStatus),
|
||||||
cursor,
|
cursor,
|
||||||
cursorKey: resolveCursorKey(chatJid, taskStatus),
|
cursorKey: resolveCursorKey(chatJid, taskStatus),
|
||||||
@@ -988,28 +883,26 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (taskStatus === 'merge_ready') {
|
if (nextTurnAction.kind === 'finalize-owner-turn') {
|
||||||
return {
|
return {
|
||||||
prompt: buildFinalizePendingPrompt(task),
|
prompt: buildFinalizePendingPrompt({ turnOutputs }),
|
||||||
channel: resolveChannel(taskStatus),
|
channel: resolveChannel(taskStatus),
|
||||||
cursor,
|
cursor,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (taskStatus === 'active') {
|
if (nextTurnAction.kind === 'owner-follow-up') {
|
||||||
const turnOutputs = getPairedTurnOutputs(task.id);
|
return {
|
||||||
const lastTurnOutput = turnOutputs[turnOutputs.length - 1];
|
prompt: buildOwnerPendingPrompt({
|
||||||
if (
|
chatJid,
|
||||||
lastTurnOutput &&
|
timezone: deps.timezone,
|
||||||
(lastTurnOutput.role === 'reviewer' ||
|
turnOutputs,
|
||||||
lastTurnOutput.role === 'arbiter')
|
recentHumanMessages,
|
||||||
) {
|
lastHumanMessage,
|
||||||
return {
|
}),
|
||||||
prompt: buildOwnerPendingPrompt(task, chatJid, deps.timezone),
|
channel: resolveChannel(taskStatus),
|
||||||
channel: resolveChannel(taskStatus),
|
cursor,
|
||||||
cursor,
|
};
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -1068,7 +961,9 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
}): boolean =>
|
}): boolean =>
|
||||||
hasReviewerLease(chatJid) &&
|
hasReviewerLease(chatJid) &&
|
||||||
args.deliveryRole === 'reviewer' &&
|
args.deliveryRole === 'reviewer' &&
|
||||||
args.pendingTask?.status === 'merge_ready';
|
resolveNextTurnAction({
|
||||||
|
taskStatus: args.pendingTask?.status ?? null,
|
||||||
|
}).kind === 'finalize-owner-turn';
|
||||||
|
|
||||||
const enqueueGenericFollowUpAfterDeliveryRetry = (args: {
|
const enqueueGenericFollowUpAfterDeliveryRetry = (args: {
|
||||||
deliveryRole: PairedRoomRole;
|
deliveryRole: PairedRoomRole;
|
||||||
@@ -1301,18 +1196,24 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
const turnRole = resolveActiveRole(taskStatus);
|
const turnRole = resolveActiveRole(taskStatus);
|
||||||
let prompt: string;
|
let prompt: string;
|
||||||
if (turnRole === 'arbiter' && pendingTaskForChannel) {
|
if (turnRole === 'arbiter' && pendingTaskForChannel) {
|
||||||
prompt = buildArbiterPromptForTask(
|
const recentMessages = getRecentChatMessages(chatJid, 20);
|
||||||
pendingTaskForChannel,
|
prompt = buildArbiterPromptForTask({
|
||||||
|
task: pendingTaskForChannel,
|
||||||
chatJid,
|
chatJid,
|
||||||
deps.timezone,
|
timezone: deps.timezone,
|
||||||
);
|
turnOutputs: getPairedTurnOutputs(pendingTaskForChannel.id),
|
||||||
|
recentMessages,
|
||||||
|
labeledRecentMessages: labelPairedSenders(chatJid, recentMessages),
|
||||||
|
});
|
||||||
} else if (pendingTaskForChannel) {
|
} else if (pendingTaskForChannel) {
|
||||||
prompt = buildPairedTurnPrompt(
|
prompt = buildPairedTurnPrompt({
|
||||||
pendingTaskForChannel.id,
|
taskId: pendingTaskForChannel.id,
|
||||||
chatJid,
|
chatJid,
|
||||||
deps.timezone,
|
timezone: deps.timezone,
|
||||||
missedMessages,
|
missedMessages,
|
||||||
);
|
labeledFallbackMessages: labelPairedSenders(chatJid, missedMessages),
|
||||||
|
turnOutputs: getPairedTurnOutputs(pendingTaskForChannel.id),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
prompt = formatMessages(
|
prompt = formatMessages(
|
||||||
labelPairedSenders(chatJid, missedMessages),
|
labelPairedSenders(chatJid, missedMessages),
|
||||||
@@ -1538,12 +1439,17 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
? pendingMessages
|
? pendingMessages
|
||||||
: processableGroupMessages;
|
: processableGroupMessages;
|
||||||
const formatted = loopPendingTask
|
const formatted = loopPendingTask
|
||||||
? buildPairedTurnPrompt(
|
? buildPairedTurnPrompt({
|
||||||
loopPendingTask.id,
|
taskId: loopPendingTask.id,
|
||||||
chatJid,
|
chatJid,
|
||||||
deps.timezone,
|
timezone: deps.timezone,
|
||||||
messagesToSend,
|
missedMessages: messagesToSend,
|
||||||
)
|
labeledFallbackMessages: labelPairedSenders(
|
||||||
|
chatJid,
|
||||||
|
messagesToSend,
|
||||||
|
),
|
||||||
|
turnOutputs: getPairedTurnOutputs(loopPendingTask.id),
|
||||||
|
})
|
||||||
: formatMessages(
|
: formatMessages(
|
||||||
labelPairedSenders(chatJid, messagesToSend),
|
labelPairedSenders(chatJid, messagesToSend),
|
||||||
deps.timezone,
|
deps.timezone,
|
||||||
|
|||||||
Reference in New Issue
Block a user