refactor: split message runtime processing
This commit is contained in:
@@ -160,10 +160,6 @@
|
|||||||
"maxNesting": 1,
|
"maxNesting": 1,
|
||||||
"owner": "message runtime test hotspot"
|
"owner": "message runtime test hotspot"
|
||||||
},
|
},
|
||||||
"src/message-runtime.ts": {
|
|
||||||
"maxFunctionLines": 620,
|
|
||||||
"owner": "message runtime hotspot"
|
|
||||||
},
|
|
||||||
"src/message-turn-controller.test.ts": {
|
"src/message-turn-controller.test.ts": {
|
||||||
"maxFunctionLines": 730,
|
"maxFunctionLines": 730,
|
||||||
"owner": "message turn controller test hotspot"
|
"owner": "message turn controller test hotspot"
|
||||||
|
|||||||
499
src/message-runtime-group-processing.ts
Normal file
499
src/message-runtime-group-processing.ts
Normal file
@@ -0,0 +1,499 @@
|
|||||||
|
import {
|
||||||
|
getLatestOpenPairedTaskForChat,
|
||||||
|
getMessagesSinceSeq,
|
||||||
|
getOpenWorkItemForChat,
|
||||||
|
markWorkItemDelivered,
|
||||||
|
type WorkItem,
|
||||||
|
} from './db.js';
|
||||||
|
import {
|
||||||
|
isSessionCommandSenderAllowed,
|
||||||
|
SERVICE_SESSION_SCOPE,
|
||||||
|
} from './config.js';
|
||||||
|
import { resolveRuntimeAttachmentBaseDirs } from './attachment-base-dirs.js';
|
||||||
|
import { enqueueGenericFollowUpAfterDeliveryRetry as enqueueDeliveryRetryFollowUp } from './message-runtime-dispatch.js';
|
||||||
|
import { processOpenWorkItemDelivery } from './message-runtime-delivery.js';
|
||||||
|
import { handleQueuedRunGates } from './message-runtime-gating.js';
|
||||||
|
import {
|
||||||
|
runPendingPairedTurnIfNeeded,
|
||||||
|
runQueuedGroupTurn,
|
||||||
|
} from './message-runtime-queue.js';
|
||||||
|
import {
|
||||||
|
advanceLastAgentCursor,
|
||||||
|
filterLoopingPairedBotMessages,
|
||||||
|
getProcessableMessages,
|
||||||
|
shouldSkipBotOnlyCollaboration,
|
||||||
|
} from './message-runtime-rules.js';
|
||||||
|
import { resolveOwnerTaskForHumanMessage } from './paired-execution-context.js';
|
||||||
|
import { transitionPairedTaskStatus } from './paired-task-status.js';
|
||||||
|
import { hasReviewerLease } from './service-routing.js';
|
||||||
|
import {
|
||||||
|
getFixedRoleChannelName,
|
||||||
|
getMissingRoleChannelMessage,
|
||||||
|
} from './message-runtime-shared.js';
|
||||||
|
import { resolvePairedRoleChannels } from './message-runtime-role-channels.js';
|
||||||
|
import {
|
||||||
|
getFreshHumanPreflightMessages,
|
||||||
|
hasHumanMessageAfterWorkItem,
|
||||||
|
} from './message-runtime-preflight-messages.js';
|
||||||
|
import { deliverCanonicalOutboundMessage } from './ipc-outbound-delivery.js';
|
||||||
|
import { findChannel, formatMessages } from './router.js';
|
||||||
|
import { createScopedLogger, logger } from './logger.js';
|
||||||
|
import type { AgentOutput } from './agent-runner.js';
|
||||||
|
import type { GroupQueue, GroupRunContext } from './group-queue.js';
|
||||||
|
import type {
|
||||||
|
ExecuteTurnFn,
|
||||||
|
RoleToChannelMap,
|
||||||
|
} from './message-runtime-types.js';
|
||||||
|
import type {
|
||||||
|
Channel,
|
||||||
|
NewMessage,
|
||||||
|
PairedTask,
|
||||||
|
RegisteredGroup,
|
||||||
|
} from './types.js';
|
||||||
|
|
||||||
|
type RunAgentFn = (
|
||||||
|
group: RegisteredGroup,
|
||||||
|
prompt: string,
|
||||||
|
chatJid: string,
|
||||||
|
runId: string,
|
||||||
|
onOutput?: (output: AgentOutput) => Promise<void>,
|
||||||
|
) => Promise<'success' | 'error'>;
|
||||||
|
|
||||||
|
interface ProcessGroupMessagesDeps {
|
||||||
|
assistantName: string;
|
||||||
|
failureFinalText: string;
|
||||||
|
timezone: string;
|
||||||
|
triggerPattern: RegExp;
|
||||||
|
channels: Channel[];
|
||||||
|
queue: Pick<GroupQueue, 'enqueueMessageCheck' | 'closeStdin' | 'killProcess'>;
|
||||||
|
getRoomBindings: () => Record<string, RegisteredGroup>;
|
||||||
|
getLastAgentTimestamps: () => Record<string, string>;
|
||||||
|
saveState: () => void;
|
||||||
|
clearSession: (groupFolder: string, opts?: { allRoles?: boolean }) => void;
|
||||||
|
runAgent: RunAgentFn;
|
||||||
|
executeTurn: ExecuteTurnFn;
|
||||||
|
hasImplicitContinuationWindow: (
|
||||||
|
chatJid: string,
|
||||||
|
messages: NewMessage[],
|
||||||
|
) => boolean;
|
||||||
|
openContinuation: (chatJid: string) => void;
|
||||||
|
isDuplicateOfLastBotFinal: (chatJid: string, text: string) => boolean;
|
||||||
|
labelPairedSenders: (chatJid: string, messages: NewMessage[]) => NewMessage[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RuntimeContext {
|
||||||
|
chatJid: string;
|
||||||
|
runId: string;
|
||||||
|
group: RegisteredGroup;
|
||||||
|
channel: Channel;
|
||||||
|
log: ReturnType<typeof createScopedLogger>;
|
||||||
|
roleToChannel: RoleToChannelMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PendingDeliveryState {
|
||||||
|
pendingTask: PairedTask | null;
|
||||||
|
openWorkItem: WorkItem | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createProcessGroupMessages(
|
||||||
|
args: ProcessGroupMessagesDeps,
|
||||||
|
): (chatJid: string, context: GroupRunContext) => Promise<boolean> {
|
||||||
|
return async (chatJid, context): Promise<boolean> => {
|
||||||
|
const runtime = resolveRuntimeContext(args, chatJid, context.runId);
|
||||||
|
if (!runtime) return true;
|
||||||
|
|
||||||
|
const pendingDelivery = resolvePendingDeliveryState(args, runtime);
|
||||||
|
const deliveryOutcome = await deliverPendingWorkItem(
|
||||||
|
args,
|
||||||
|
runtime,
|
||||||
|
pendingDelivery,
|
||||||
|
);
|
||||||
|
if (deliveryOutcome !== null) {
|
||||||
|
return deliveryOutcome;
|
||||||
|
}
|
||||||
|
|
||||||
|
return processMissedMessages(args, runtime);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRuntimeContext(
|
||||||
|
args: ProcessGroupMessagesDeps,
|
||||||
|
chatJid: string,
|
||||||
|
runId: string,
|
||||||
|
): RuntimeContext | null {
|
||||||
|
const group = args.getRoomBindings()[chatJid];
|
||||||
|
if (!group) return null;
|
||||||
|
const log = createScopedLogger({
|
||||||
|
chatJid,
|
||||||
|
groupName: group.name,
|
||||||
|
groupFolder: group.folder,
|
||||||
|
runId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const channel = findChannel(args.channels, chatJid);
|
||||||
|
if (!channel) {
|
||||||
|
log.warn('No channel owns JID, skipping messages');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
roleToChannel,
|
||||||
|
reviewerChannelName,
|
||||||
|
foundReviewerChannel,
|
||||||
|
arbiterChannelName,
|
||||||
|
foundArbiterChannel,
|
||||||
|
} = resolvePairedRoleChannels(args.channels, channel);
|
||||||
|
if (hasReviewerLease(chatJid)) {
|
||||||
|
log.info(
|
||||||
|
{
|
||||||
|
reviewerChannelName,
|
||||||
|
foundChannel: foundReviewerChannel?.name ?? null,
|
||||||
|
arbiterChannelName,
|
||||||
|
foundArbiterChannel: foundArbiterChannel?.name ?? null,
|
||||||
|
availableChannels: args.channels.map((c) => c.name),
|
||||||
|
},
|
||||||
|
'Paired room reviewer/arbiter channel resolution',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { chatJid, runId, group, channel, log, roleToChannel };
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePendingDeliveryState(
|
||||||
|
args: ProcessGroupMessagesDeps,
|
||||||
|
runtime: RuntimeContext,
|
||||||
|
): PendingDeliveryState {
|
||||||
|
const { chatJid, channel, group, log } = runtime;
|
||||||
|
let pendingTask: PairedTask | null = hasReviewerLease(chatJid)
|
||||||
|
? (getLatestOpenPairedTaskForChat(chatJid) ?? null)
|
||||||
|
: null;
|
||||||
|
let openWorkItem = getOpenWorkItemForChat(chatJid, SERVICE_SESSION_SCOPE);
|
||||||
|
if (openWorkItem?.delivery_role !== 'owner' || !pendingTask) {
|
||||||
|
return { pendingTask, openWorkItem };
|
||||||
|
}
|
||||||
|
|
||||||
|
const freshHumanMessages = getFreshHumanPreflightMessages({
|
||||||
|
chatJid,
|
||||||
|
channel,
|
||||||
|
lastAgentTimestamps: args.getLastAgentTimestamps(),
|
||||||
|
assistantName: args.assistantName,
|
||||||
|
failureFinalText: args.failureFinalText,
|
||||||
|
});
|
||||||
|
if (pendingTask.status === 'merge_ready' && freshHumanMessages.length > 0) {
|
||||||
|
const resolvedTask = resolveOwnerTaskForHumanMessage({
|
||||||
|
group,
|
||||||
|
chatJid,
|
||||||
|
existingTask: pendingTask,
|
||||||
|
});
|
||||||
|
pendingTask = resolvedTask.task ?? null;
|
||||||
|
if (resolvedTask.supersededTask) {
|
||||||
|
markWorkItemDelivered(openWorkItem.id);
|
||||||
|
log.info(
|
||||||
|
{
|
||||||
|
chatJid,
|
||||||
|
workItemId: openWorkItem.id,
|
||||||
|
supersededTaskId: resolvedTask.supersededTask.id,
|
||||||
|
replacementTaskId: resolvedTask.task?.id ?? null,
|
||||||
|
},
|
||||||
|
'Suppressed stale owner delivery retry because a new human message superseded the merge_ready task',
|
||||||
|
);
|
||||||
|
openWorkItem = undefined;
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
pendingTask.status === 'active' &&
|
||||||
|
hasHumanMessageAfterWorkItem(openWorkItem, freshHumanMessages)
|
||||||
|
) {
|
||||||
|
markWorkItemDelivered(openWorkItem.id);
|
||||||
|
log.info(
|
||||||
|
{
|
||||||
|
chatJid,
|
||||||
|
workItemId: openWorkItem.id,
|
||||||
|
taskId: pendingTask.id,
|
||||||
|
taskStatus: pendingTask.status,
|
||||||
|
workItemEndSeq: openWorkItem.end_seq ?? null,
|
||||||
|
freshHumanMessageCount: freshHumanMessages.length,
|
||||||
|
},
|
||||||
|
'Suppressed stale owner delivery retry because a new human message arrived while the paired task was still active',
|
||||||
|
);
|
||||||
|
openWorkItem = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { pendingTask, openWorkItem };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deliverPendingWorkItem(
|
||||||
|
args: ProcessGroupMessagesDeps,
|
||||||
|
runtime: RuntimeContext,
|
||||||
|
pendingDelivery: PendingDeliveryState,
|
||||||
|
): Promise<boolean | null> {
|
||||||
|
const { chatJid, runId, group, channel, roleToChannel, log } = runtime;
|
||||||
|
const openWorkItemOutcome = await processOpenWorkItemDelivery({
|
||||||
|
chatJid,
|
||||||
|
runId,
|
||||||
|
openWorkItem: pendingDelivery.openWorkItem,
|
||||||
|
pendingTask: pendingDelivery.pendingTask,
|
||||||
|
channel,
|
||||||
|
roleToChannel,
|
||||||
|
log,
|
||||||
|
attachmentBaseDirs: resolveRuntimeAttachmentBaseDirs(group),
|
||||||
|
isPairedRoom: hasReviewerLease(chatJid),
|
||||||
|
getMissingRoleChannelMessage,
|
||||||
|
isDuplicateOfLastBotFinal: args.isDuplicateOfLastBotFinal,
|
||||||
|
openContinuation: args.openContinuation,
|
||||||
|
enqueueFollowUpAfterDeliveryRetry: ({
|
||||||
|
deliveryRole,
|
||||||
|
pendingTask,
|
||||||
|
workItemId,
|
||||||
|
}) =>
|
||||||
|
enqueueDeliveryRetryFollowUp({
|
||||||
|
chatJid,
|
||||||
|
runId,
|
||||||
|
deliveryRole,
|
||||||
|
pendingTask,
|
||||||
|
workItemId,
|
||||||
|
log,
|
||||||
|
enqueueMessageCheck: () => args.queue.enqueueMessageCheck(chatJid),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (openWorkItemOutcome === 'failed') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (openWorkItemOutcome === 'delivered') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processMissedMessages(
|
||||||
|
args: ProcessGroupMessagesDeps,
|
||||||
|
runtime: RuntimeContext,
|
||||||
|
): Promise<boolean> {
|
||||||
|
while (true) {
|
||||||
|
const rawMissedMessages = getMessagesSinceSeq(
|
||||||
|
runtime.chatJid,
|
||||||
|
args.getLastAgentTimestamps()[runtime.chatJid] || '0',
|
||||||
|
args.assistantName,
|
||||||
|
);
|
||||||
|
const missedMessages = filterLoopingPairedBotMessages(
|
||||||
|
runtime.chatJid,
|
||||||
|
getProcessableMessages(
|
||||||
|
runtime.chatJid,
|
||||||
|
rawMissedMessages,
|
||||||
|
runtime.channel,
|
||||||
|
),
|
||||||
|
args.failureFinalText,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (missedMessages.length === 0) {
|
||||||
|
return handleNoMissedMessages(args, runtime, rawMissedMessages);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pendingTurnOutcome = await runBotOnlyPendingTurn(
|
||||||
|
args,
|
||||||
|
runtime,
|
||||||
|
rawMissedMessages,
|
||||||
|
missedMessages,
|
||||||
|
);
|
||||||
|
if (pendingTurnOutcome !== null) {
|
||||||
|
return pendingTurnOutcome;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldSkipBotOnlyCollaboration(runtime.chatJid, missedMessages)) {
|
||||||
|
advancePastBotOnlyCollaboration(args, runtime, missedMessages);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gateOutcome = await runQueuedRunGates(args, runtime, missedMessages);
|
||||||
|
if (gateOutcome !== null) {
|
||||||
|
return gateOutcome;
|
||||||
|
}
|
||||||
|
|
||||||
|
return runQueuedGroupTurn({
|
||||||
|
chatJid: runtime.chatJid,
|
||||||
|
group: runtime.group,
|
||||||
|
runId: runtime.runId,
|
||||||
|
log: runtime.log,
|
||||||
|
timezone: args.timezone,
|
||||||
|
missedMessages,
|
||||||
|
task: hasReviewerLease(runtime.chatJid)
|
||||||
|
? (getLatestOpenPairedTaskForChat(runtime.chatJid) ?? null)
|
||||||
|
: undefined,
|
||||||
|
roleToChannel: runtime.roleToChannel,
|
||||||
|
ownerChannel: runtime.channel,
|
||||||
|
lastAgentTimestamps: args.getLastAgentTimestamps(),
|
||||||
|
saveState: args.saveState,
|
||||||
|
executeTurn: args.executeTurn,
|
||||||
|
getFixedRoleChannelName,
|
||||||
|
labelPairedSenders: args.labelPairedSenders,
|
||||||
|
formatMessages,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleNoMissedMessages(
|
||||||
|
args: ProcessGroupMessagesDeps,
|
||||||
|
runtime: RuntimeContext,
|
||||||
|
rawMissedMessages: NewMessage[],
|
||||||
|
): Promise<boolean> {
|
||||||
|
const pendingTurnOutcome = await runPendingPairedTurnIfNeeded({
|
||||||
|
chatJid: runtime.chatJid,
|
||||||
|
group: runtime.group,
|
||||||
|
runId: runtime.runId,
|
||||||
|
log: runtime.log,
|
||||||
|
timezone: args.timezone,
|
||||||
|
task: hasReviewerLease(runtime.chatJid)
|
||||||
|
? getLatestOpenPairedTaskForChat(runtime.chatJid)
|
||||||
|
: null,
|
||||||
|
rawMissedMessages,
|
||||||
|
saveState: args.saveState,
|
||||||
|
lastAgentTimestamps: args.getLastAgentTimestamps(),
|
||||||
|
executeTurn: args.executeTurn,
|
||||||
|
getFixedRoleChannelName,
|
||||||
|
roleToChannel: runtime.roleToChannel,
|
||||||
|
labelPairedSenders: args.labelPairedSenders,
|
||||||
|
mode: 'idle',
|
||||||
|
});
|
||||||
|
if (pendingTurnOutcome !== null) {
|
||||||
|
return pendingTurnOutcome;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastIgnored = rawMissedMessages[rawMissedMessages.length - 1];
|
||||||
|
if (lastIgnored) {
|
||||||
|
advanceLastAgentCursor(
|
||||||
|
args.getLastAgentTimestamps(),
|
||||||
|
args.saveState,
|
||||||
|
runtime.chatJid,
|
||||||
|
lastIgnored.timestamp,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runBotOnlyPendingTurn(
|
||||||
|
args: ProcessGroupMessagesDeps,
|
||||||
|
runtime: RuntimeContext,
|
||||||
|
rawMissedMessages: NewMessage[],
|
||||||
|
missedMessages: NewMessage[],
|
||||||
|
): Promise<boolean | null> {
|
||||||
|
return runPendingPairedTurnIfNeeded({
|
||||||
|
chatJid: runtime.chatJid,
|
||||||
|
group: runtime.group,
|
||||||
|
runId: runtime.runId,
|
||||||
|
log: runtime.log,
|
||||||
|
timezone: args.timezone,
|
||||||
|
task: hasReviewerLease(runtime.chatJid)
|
||||||
|
? getLatestOpenPairedTaskForChat(runtime.chatJid)
|
||||||
|
: null,
|
||||||
|
rawMissedMessages,
|
||||||
|
saveState: args.saveState,
|
||||||
|
lastAgentTimestamps: args.getLastAgentTimestamps(),
|
||||||
|
executeTurn: args.executeTurn,
|
||||||
|
getFixedRoleChannelName,
|
||||||
|
roleToChannel: runtime.roleToChannel,
|
||||||
|
labelPairedSenders: args.labelPairedSenders,
|
||||||
|
mode: 'bot-only',
|
||||||
|
missedMessages,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function advancePastBotOnlyCollaboration(
|
||||||
|
args: ProcessGroupMessagesDeps,
|
||||||
|
runtime: RuntimeContext,
|
||||||
|
missedMessages: NewMessage[],
|
||||||
|
): void {
|
||||||
|
const lastMessage = missedMessages[missedMessages.length - 1];
|
||||||
|
if (lastMessage?.seq != null) {
|
||||||
|
advanceLastAgentCursor(
|
||||||
|
args.getLastAgentTimestamps(),
|
||||||
|
args.saveState,
|
||||||
|
runtime.chatJid,
|
||||||
|
lastMessage.seq,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
runtime.log.info(
|
||||||
|
'Skipping bot-only collaboration because no recent human message exists',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runQueuedRunGates(
|
||||||
|
args: ProcessGroupMessagesDeps,
|
||||||
|
runtime: RuntimeContext,
|
||||||
|
missedMessages: NewMessage[],
|
||||||
|
): Promise<boolean | null> {
|
||||||
|
const gateResult = await handleQueuedRunGates({
|
||||||
|
chatJid: runtime.chatJid,
|
||||||
|
group: runtime.group,
|
||||||
|
runId: runtime.runId,
|
||||||
|
missedMessages,
|
||||||
|
triggerPattern: args.triggerPattern,
|
||||||
|
timezone: args.timezone,
|
||||||
|
hasImplicitContinuationWindow: args.hasImplicitContinuationWindow,
|
||||||
|
sessionCommandDeps: {
|
||||||
|
sendMessage: (text) =>
|
||||||
|
deliverSessionCommandMessage(args, runtime.chatJid, text),
|
||||||
|
setTyping: (typing) =>
|
||||||
|
runtime.channel.setTyping?.(runtime.chatJid, typing) ??
|
||||||
|
Promise.resolve(),
|
||||||
|
runAgent: (prompt, onOutput) =>
|
||||||
|
args.runAgent(
|
||||||
|
runtime.group,
|
||||||
|
prompt,
|
||||||
|
runtime.chatJid,
|
||||||
|
runtime.runId,
|
||||||
|
onOutput,
|
||||||
|
),
|
||||||
|
closeStdin: () =>
|
||||||
|
args.queue.closeStdin(runtime.chatJid, {
|
||||||
|
reason: 'session-command',
|
||||||
|
}),
|
||||||
|
clearSession: (opts) => args.clearSession(runtime.group.folder, opts),
|
||||||
|
advanceCursor: (cursorOrTimestamp) => {
|
||||||
|
advanceLastAgentCursor(
|
||||||
|
args.getLastAgentTimestamps(),
|
||||||
|
args.saveState,
|
||||||
|
runtime.chatJid,
|
||||||
|
cursorOrTimestamp,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
formatMessages,
|
||||||
|
isAdminSender: (msg) => isSessionCommandSenderAllowed(msg.sender),
|
||||||
|
canSenderInteract: () => true,
|
||||||
|
resetPairedTask: () => completePairedTaskAsStopped(runtime.chatJid),
|
||||||
|
killProcess: () => args.queue.killProcess(runtime.chatJid),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return gateResult.handled ? gateResult.success : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function completePairedTaskAsStopped(chatJid: string): void {
|
||||||
|
if (!hasReviewerLease(chatJid)) return;
|
||||||
|
const task = getLatestOpenPairedTaskForChat(chatJid);
|
||||||
|
if (!task) return;
|
||||||
|
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
transitionPairedTaskStatus({
|
||||||
|
taskId: task.id,
|
||||||
|
currentStatus: task.status,
|
||||||
|
nextStatus: 'completed',
|
||||||
|
expectedUpdatedAt: task.updated_at,
|
||||||
|
updatedAt: now,
|
||||||
|
patch: {
|
||||||
|
completion_reason: 'stopped',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deliverSessionCommandMessage(
|
||||||
|
deps: Pick<ProcessGroupMessagesDeps, 'channels' | 'getRoomBindings'>,
|
||||||
|
chatJid: string,
|
||||||
|
text: string,
|
||||||
|
): Promise<void> {
|
||||||
|
await deliverCanonicalOutboundMessage(
|
||||||
|
{ jid: chatJid, text },
|
||||||
|
{
|
||||||
|
channels: deps.channels,
|
||||||
|
roomBindings: deps.getRoomBindings,
|
||||||
|
log: logger,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,61 +1,24 @@
|
|||||||
import {
|
import { updatePairedTurnProgressText } from './db.js';
|
||||||
getOpenWorkItemForChat,
|
|
||||||
getMessagesSinceSeq,
|
|
||||||
getLatestOpenPairedTaskForChat,
|
|
||||||
markWorkItemDelivered,
|
|
||||||
updatePairedTurnProgressText,
|
|
||||||
} from './db.js';
|
|
||||||
import {
|
|
||||||
isSessionCommandSenderAllowed,
|
|
||||||
SERVICE_SESSION_SCOPE,
|
|
||||||
} from './config.js';
|
|
||||||
import { resolveRuntimeAttachmentBaseDirs } from './attachment-base-dirs.js';
|
|
||||||
import { GroupQueue, GroupRunContext } from './group-queue.js';
|
import { GroupQueue, GroupRunContext } from './group-queue.js';
|
||||||
import { findChannel, formatMessages } from './router.js';
|
|
||||||
import { enqueueGenericFollowUpAfterDeliveryRetry as enqueueDeliveryRetryFollowUp } from './message-runtime-dispatch.js';
|
|
||||||
import { processOpenWorkItemDelivery } from './message-runtime-delivery.js';
|
|
||||||
import { deliverCanonicalOutboundMessage } from './ipc-outbound-delivery.js';
|
|
||||||
import { handleQueuedRunGates } from './message-runtime-gating.js';
|
|
||||||
import { enqueueMessageRuntimePendingHandoffs } from './message-runtime-handoffs.js';
|
import { enqueueMessageRuntimePendingHandoffs } from './message-runtime-handoffs.js';
|
||||||
import {
|
import {
|
||||||
buildScopedMessageCheckEnqueuer,
|
buildScopedMessageCheckEnqueuer,
|
||||||
processMessageLoopTick,
|
processMessageLoopTick,
|
||||||
recoverPendingMessages as recoverRuntimePendingMessages,
|
recoverPendingMessages as recoverRuntimePendingMessages,
|
||||||
} from './message-runtime-loop.js';
|
} from './message-runtime-loop.js';
|
||||||
import {
|
import { createImplicitContinuationTracker } from './message-runtime-rules.js';
|
||||||
runPendingPairedTurnIfNeeded,
|
|
||||||
runQueuedGroupTurn,
|
|
||||||
} from './message-runtime-queue.js';
|
|
||||||
import {
|
|
||||||
advanceLastAgentCursor,
|
|
||||||
createImplicitContinuationTracker,
|
|
||||||
filterLoopingPairedBotMessages,
|
|
||||||
getProcessableMessages,
|
|
||||||
shouldSkipBotOnlyCollaboration,
|
|
||||||
} from './message-runtime-rules.js';
|
|
||||||
import { resolveOwnerTaskForHumanMessage } from './paired-execution-context.js';
|
|
||||||
import { schedulePairedFollowUpWithMessageCheck } from './message-runtime-follow-up.js';
|
import { schedulePairedFollowUpWithMessageCheck } from './message-runtime-follow-up.js';
|
||||||
import { transitionPairedTaskStatus } from './paired-task-status.js';
|
|
||||||
import { Channel, NewMessage, RegisteredGroup } from './types.js';
|
import { Channel, NewMessage, RegisteredGroup } from './types.js';
|
||||||
import { createScopedLogger, logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
import { hasReviewerLease } from './service-routing.js';
|
|
||||||
import {
|
|
||||||
getFixedRoleChannelName,
|
|
||||||
getMissingRoleChannelMessage,
|
|
||||||
} from './message-runtime-shared.js';
|
|
||||||
import {
|
import {
|
||||||
createRunAgent,
|
createRunAgent,
|
||||||
createExecuteTurn,
|
createExecuteTurn,
|
||||||
isDuplicateOfLastBotFinal,
|
isDuplicateOfLastBotFinal,
|
||||||
labelPairedSenders,
|
labelPairedSenders,
|
||||||
} from './message-runtime-turns.js';
|
} from './message-runtime-turns.js';
|
||||||
import { resolvePairedRoleChannels } from './message-runtime-role-channels.js';
|
|
||||||
import {
|
|
||||||
getFreshHumanPreflightMessages,
|
|
||||||
hasHumanMessageAfterWorkItem,
|
|
||||||
} from './message-runtime-preflight-messages.js';
|
|
||||||
import { handleMessageRuntimeAfterDeliverySuccess } from './message-runtime-after-delivery.js';
|
import { handleMessageRuntimeAfterDeliverySuccess } from './message-runtime-after-delivery.js';
|
||||||
import { deliverMessageRuntimeFinalText } from './message-runtime-final-delivery.js';
|
import { deliverMessageRuntimeFinalText } from './message-runtime-final-delivery.js';
|
||||||
|
import { createProcessGroupMessages } from './message-runtime-group-processing.js';
|
||||||
|
|
||||||
export { isDuplicateOfLastBotFinal };
|
export { isDuplicateOfLastBotFinal };
|
||||||
|
|
||||||
@@ -77,19 +40,30 @@ export interface MessageRuntimeDeps {
|
|||||||
clearSession: (groupFolder: string, opts?: { allRoles?: boolean }) => void;
|
clearSession: (groupFolder: string, opts?: { allRoles?: boolean }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deliverSessionCommandMessage(
|
function createStartMessageLoop(args: {
|
||||||
deps: Pick<MessageRuntimeDeps, 'channels' | 'getRoomBindings'>,
|
pollInterval: number;
|
||||||
chatJid: string,
|
processTick: () => Promise<void>;
|
||||||
text: string,
|
}): () => Promise<void> {
|
||||||
): Promise<void> {
|
let messageLoopRunning = false;
|
||||||
await deliverCanonicalOutboundMessage(
|
|
||||||
{ jid: chatJid, text },
|
return async (): Promise<void> => {
|
||||||
{
|
if (messageLoopRunning) {
|
||||||
channels: deps.channels,
|
logger.debug('Message loop already running, skipping duplicate start');
|
||||||
roomBindings: deps.getRoomBindings,
|
return;
|
||||||
log: logger,
|
}
|
||||||
},
|
messageLoopRunning = true;
|
||||||
);
|
|
||||||
|
logger.info('EJClaw running');
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
await args.processTick();
|
||||||
|
} catch (err) {
|
||||||
|
logger.error({ err }, 'Error in message loop');
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, args.pollInterval));
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
||||||
@@ -100,7 +74,6 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
recoverPendingMessages: () => void;
|
recoverPendingMessages: () => void;
|
||||||
startMessageLoop: () => Promise<void>;
|
startMessageLoop: () => Promise<void>;
|
||||||
} {
|
} {
|
||||||
let messageLoopRunning = false;
|
|
||||||
const FAILURE_FINAL_TEXT = '요청을 완료하지 못했습니다. 다시 시도해 주세요.';
|
const FAILURE_FINAL_TEXT = '요청을 완료하지 못했습니다. 다시 시도해 주세요.';
|
||||||
const continuationTracker = createImplicitContinuationTracker(
|
const continuationTracker = createImplicitContinuationTracker(
|
||||||
deps.idleTimeout,
|
deps.idleTimeout,
|
||||||
@@ -113,10 +86,6 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
deps.queue,
|
deps.queue,
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a message is a duplicate of the last bot final message in a paired room.
|
|
||||||
* Returns true if duplicate (should be suppressed).
|
|
||||||
*/
|
|
||||||
const checkDuplicateOfLastBotFinal = (
|
const checkDuplicateOfLastBotFinal = (
|
||||||
chatJid: string,
|
chatJid: string,
|
||||||
text: string,
|
text: string,
|
||||||
@@ -208,314 +177,28 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const processGroupMessages = async (
|
const processGroupMessages = createProcessGroupMessages({
|
||||||
chatJid: string,
|
|
||||||
context: GroupRunContext,
|
|
||||||
): Promise<boolean> => {
|
|
||||||
const { runId } = context;
|
|
||||||
const group = deps.getRoomBindings()[chatJid];
|
|
||||||
if (!group) return true;
|
|
||||||
const log = createScopedLogger({
|
|
||||||
chatJid,
|
|
||||||
groupName: group.name,
|
|
||||||
groupFolder: group.folder,
|
|
||||||
runId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const channel = findChannel(deps.channels, chatJid);
|
|
||||||
if (!channel) {
|
|
||||||
log.warn('No channel owns JID, skipping messages');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
roleToChannel,
|
|
||||||
reviewerChannelName,
|
|
||||||
foundReviewerChannel,
|
|
||||||
arbiterChannelName,
|
|
||||||
foundArbiterChannel,
|
|
||||||
} = resolvePairedRoleChannels(deps.channels, channel);
|
|
||||||
if (hasReviewerLease(chatJid)) {
|
|
||||||
log.info(
|
|
||||||
{
|
|
||||||
reviewerChannelName,
|
|
||||||
foundChannel: foundReviewerChannel?.name ?? null,
|
|
||||||
arbiterChannelName,
|
|
||||||
foundArbiterChannel: foundArbiterChannel?.name ?? null,
|
|
||||||
availableChannels: deps.channels.map((c) => c.name),
|
|
||||||
},
|
|
||||||
'Paired room reviewer/arbiter channel resolution',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delivery retries can come from forced fallback runs whose agent_type
|
|
||||||
// differs from the room owner's registered agent type.
|
|
||||||
let pendingTask = hasReviewerLease(chatJid)
|
|
||||||
? getLatestOpenPairedTaskForChat(chatJid)
|
|
||||||
: null;
|
|
||||||
let openWorkItem = getOpenWorkItemForChat(chatJid, SERVICE_SESSION_SCOPE);
|
|
||||||
if (openWorkItem?.delivery_role === 'owner' && pendingTask) {
|
|
||||||
const freshHumanMessages = getFreshHumanPreflightMessages({
|
|
||||||
chatJid,
|
|
||||||
channel,
|
|
||||||
lastAgentTimestamps: deps.getLastAgentTimestamps(),
|
|
||||||
assistantName: deps.assistantName,
|
assistantName: deps.assistantName,
|
||||||
failureFinalText: FAILURE_FINAL_TEXT,
|
failureFinalText: FAILURE_FINAL_TEXT,
|
||||||
});
|
|
||||||
if (
|
|
||||||
pendingTask.status === 'merge_ready' &&
|
|
||||||
freshHumanMessages.length > 0
|
|
||||||
) {
|
|
||||||
const resolvedTask = resolveOwnerTaskForHumanMessage({
|
|
||||||
group,
|
|
||||||
chatJid,
|
|
||||||
existingTask: pendingTask,
|
|
||||||
});
|
|
||||||
pendingTask = resolvedTask.task;
|
|
||||||
if (resolvedTask.supersededTask) {
|
|
||||||
markWorkItemDelivered(openWorkItem.id);
|
|
||||||
log.info(
|
|
||||||
{
|
|
||||||
chatJid,
|
|
||||||
workItemId: openWorkItem.id,
|
|
||||||
supersededTaskId: resolvedTask.supersededTask.id,
|
|
||||||
replacementTaskId: resolvedTask.task?.id ?? null,
|
|
||||||
},
|
|
||||||
'Suppressed stale owner delivery retry because a new human message superseded the merge_ready task',
|
|
||||||
);
|
|
||||||
openWorkItem = undefined;
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
pendingTask.status === 'active' &&
|
|
||||||
hasHumanMessageAfterWorkItem(openWorkItem, freshHumanMessages)
|
|
||||||
) {
|
|
||||||
markWorkItemDelivered(openWorkItem.id);
|
|
||||||
log.info(
|
|
||||||
{
|
|
||||||
chatJid,
|
|
||||||
workItemId: openWorkItem.id,
|
|
||||||
taskId: pendingTask.id,
|
|
||||||
taskStatus: pendingTask.status,
|
|
||||||
workItemEndSeq: openWorkItem.end_seq ?? null,
|
|
||||||
freshHumanMessageCount: freshHumanMessages.length,
|
|
||||||
},
|
|
||||||
'Suppressed stale owner delivery retry because a new human message arrived while the paired task was still active',
|
|
||||||
);
|
|
||||||
openWorkItem = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const openWorkItemOutcome = await processOpenWorkItemDelivery({
|
|
||||||
chatJid,
|
|
||||||
runId,
|
|
||||||
openWorkItem,
|
|
||||||
pendingTask,
|
|
||||||
channel,
|
|
||||||
roleToChannel,
|
|
||||||
log,
|
|
||||||
attachmentBaseDirs: resolveRuntimeAttachmentBaseDirs(group),
|
|
||||||
isPairedRoom: hasReviewerLease(chatJid),
|
|
||||||
getMissingRoleChannelMessage,
|
|
||||||
isDuplicateOfLastBotFinal: checkDuplicateOfLastBotFinal,
|
|
||||||
openContinuation: (targetChatJid) =>
|
|
||||||
continuationTracker.open(targetChatJid),
|
|
||||||
enqueueFollowUpAfterDeliveryRetry: ({
|
|
||||||
deliveryRole,
|
|
||||||
pendingTask,
|
|
||||||
workItemId,
|
|
||||||
}) =>
|
|
||||||
enqueueDeliveryRetryFollowUp({
|
|
||||||
chatJid,
|
|
||||||
runId,
|
|
||||||
deliveryRole,
|
|
||||||
pendingTask,
|
|
||||||
workItemId,
|
|
||||||
log,
|
|
||||||
enqueueMessageCheck: () => deps.queue.enqueueMessageCheck(chatJid),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
if (openWorkItemOutcome === 'failed') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (openWorkItemOutcome === 'delivered') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const sinceSeqCursor = deps.getLastAgentTimestamps()[chatJid] || '0';
|
|
||||||
const rawMissedMessages = getMessagesSinceSeq(
|
|
||||||
chatJid,
|
|
||||||
sinceSeqCursor,
|
|
||||||
deps.assistantName,
|
|
||||||
);
|
|
||||||
const missedMessages = filterLoopingPairedBotMessages(
|
|
||||||
chatJid,
|
|
||||||
getProcessableMessages(chatJid, rawMissedMessages, channel),
|
|
||||||
FAILURE_FINAL_TEXT,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (missedMessages.length === 0) {
|
|
||||||
const pendingTurnOutcome = await runPendingPairedTurnIfNeeded({
|
|
||||||
chatJid,
|
|
||||||
group,
|
|
||||||
runId,
|
|
||||||
log,
|
|
||||||
timezone: deps.timezone,
|
timezone: deps.timezone,
|
||||||
task: hasReviewerLease(chatJid)
|
|
||||||
? getLatestOpenPairedTaskForChat(chatJid)
|
|
||||||
: null,
|
|
||||||
rawMissedMessages,
|
|
||||||
saveState: deps.saveState,
|
|
||||||
lastAgentTimestamps: deps.getLastAgentTimestamps(),
|
|
||||||
executeTurn,
|
|
||||||
getFixedRoleChannelName,
|
|
||||||
roleToChannel,
|
|
||||||
labelPairedSenders: labelPairedRuntimeSenders,
|
|
||||||
mode: 'idle',
|
|
||||||
});
|
|
||||||
if (pendingTurnOutcome !== null) {
|
|
||||||
return pendingTurnOutcome;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastIgnored = rawMissedMessages[rawMissedMessages.length - 1];
|
|
||||||
if (lastIgnored) {
|
|
||||||
advanceLastAgentCursor(
|
|
||||||
deps.getLastAgentTimestamps(),
|
|
||||||
deps.saveState,
|
|
||||||
chatJid,
|
|
||||||
lastIgnored.timestamp,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const botOnlyPendingTurnOutcome = await runPendingPairedTurnIfNeeded({
|
|
||||||
chatJid,
|
|
||||||
group,
|
|
||||||
runId,
|
|
||||||
log,
|
|
||||||
timezone: deps.timezone,
|
|
||||||
task: hasReviewerLease(chatJid)
|
|
||||||
? getLatestOpenPairedTaskForChat(chatJid)
|
|
||||||
: null,
|
|
||||||
rawMissedMessages,
|
|
||||||
saveState: deps.saveState,
|
|
||||||
lastAgentTimestamps: deps.getLastAgentTimestamps(),
|
|
||||||
executeTurn,
|
|
||||||
getFixedRoleChannelName,
|
|
||||||
roleToChannel,
|
|
||||||
labelPairedSenders: labelPairedRuntimeSenders,
|
|
||||||
mode: 'bot-only',
|
|
||||||
missedMessages,
|
|
||||||
});
|
|
||||||
if (botOnlyPendingTurnOutcome !== null) {
|
|
||||||
return botOnlyPendingTurnOutcome;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldSkipBotOnlyCollaboration(chatJid, missedMessages)) {
|
|
||||||
const lastMessage = missedMessages[missedMessages.length - 1];
|
|
||||||
if (lastMessage?.seq != null) {
|
|
||||||
advanceLastAgentCursor(
|
|
||||||
deps.getLastAgentTimestamps(),
|
|
||||||
deps.saveState,
|
|
||||||
chatJid,
|
|
||||||
lastMessage.seq,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
log.info(
|
|
||||||
'Skipping bot-only collaboration because no recent human message exists',
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const gateResult = await handleQueuedRunGates({
|
|
||||||
chatJid,
|
|
||||||
group,
|
|
||||||
runId,
|
|
||||||
missedMessages,
|
|
||||||
triggerPattern: deps.triggerPattern,
|
triggerPattern: deps.triggerPattern,
|
||||||
timezone: deps.timezone,
|
channels: deps.channels,
|
||||||
hasImplicitContinuationWindow: continuationTracker.has,
|
queue: deps.queue,
|
||||||
sessionCommandDeps: {
|
getRoomBindings: deps.getRoomBindings,
|
||||||
sendMessage: (text) =>
|
getLastAgentTimestamps: deps.getLastAgentTimestamps,
|
||||||
deliverSessionCommandMessage(deps, chatJid, text),
|
|
||||||
setTyping: (typing) =>
|
|
||||||
channel.setTyping?.(chatJid, typing) ?? Promise.resolve(),
|
|
||||||
runAgent: (prompt, onOutput) =>
|
|
||||||
runAgent(group, prompt, chatJid, runId, onOutput),
|
|
||||||
closeStdin: () =>
|
|
||||||
deps.queue.closeStdin(chatJid, {
|
|
||||||
reason: 'session-command',
|
|
||||||
}),
|
|
||||||
clearSession: (opts) => deps.clearSession(group.folder, opts),
|
|
||||||
advanceCursor: (cursorOrTimestamp) => {
|
|
||||||
advanceLastAgentCursor(
|
|
||||||
deps.getLastAgentTimestamps(),
|
|
||||||
deps.saveState,
|
|
||||||
chatJid,
|
|
||||||
cursorOrTimestamp,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
formatMessages,
|
|
||||||
isAdminSender: (msg) => isSessionCommandSenderAllowed(msg.sender),
|
|
||||||
canSenderInteract: () => true,
|
|
||||||
resetPairedTask: () => {
|
|
||||||
if (hasReviewerLease(chatJid)) {
|
|
||||||
const task = getLatestOpenPairedTaskForChat(chatJid);
|
|
||||||
if (task) {
|
|
||||||
const now = new Date().toISOString();
|
|
||||||
transitionPairedTaskStatus({
|
|
||||||
taskId: task.id,
|
|
||||||
currentStatus: task.status,
|
|
||||||
nextStatus: 'completed',
|
|
||||||
expectedUpdatedAt: task.updated_at,
|
|
||||||
updatedAt: now,
|
|
||||||
patch: {
|
|
||||||
completion_reason: 'stopped',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
killProcess: () => deps.queue.killProcess(chatJid),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (gateResult.handled) {
|
|
||||||
return gateResult.success;
|
|
||||||
}
|
|
||||||
|
|
||||||
return runQueuedGroupTurn({
|
|
||||||
chatJid,
|
|
||||||
group,
|
|
||||||
runId,
|
|
||||||
log,
|
|
||||||
timezone: deps.timezone,
|
|
||||||
missedMessages,
|
|
||||||
task: hasReviewerLease(chatJid)
|
|
||||||
? (getLatestOpenPairedTaskForChat(chatJid) ?? null)
|
|
||||||
: undefined,
|
|
||||||
roleToChannel,
|
|
||||||
ownerChannel: channel,
|
|
||||||
lastAgentTimestamps: deps.getLastAgentTimestamps(),
|
|
||||||
saveState: deps.saveState,
|
saveState: deps.saveState,
|
||||||
|
clearSession: deps.clearSession,
|
||||||
|
runAgent,
|
||||||
executeTurn,
|
executeTurn,
|
||||||
getFixedRoleChannelName,
|
hasImplicitContinuationWindow: continuationTracker.has,
|
||||||
|
openContinuation: continuationTracker.open,
|
||||||
|
isDuplicateOfLastBotFinal: checkDuplicateOfLastBotFinal,
|
||||||
labelPairedSenders: labelPairedRuntimeSenders,
|
labelPairedSenders: labelPairedRuntimeSenders,
|
||||||
formatMessages,
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const startMessageLoop = async (): Promise<void> => {
|
const startMessageLoop = createStartMessageLoop({
|
||||||
if (messageLoopRunning) {
|
pollInterval: deps.pollInterval,
|
||||||
logger.debug('Message loop already running, skipping duplicate start');
|
processTick: async () => {
|
||||||
return;
|
|
||||||
}
|
|
||||||
messageLoopRunning = true;
|
|
||||||
|
|
||||||
logger.info('EJClaw running');
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
await processMessageLoopTick({
|
await processMessageLoopTick({
|
||||||
assistantName: deps.assistantName,
|
assistantName: deps.assistantName,
|
||||||
failureFinalText: FAILURE_FINAL_TEXT,
|
failureFinalText: FAILURE_FINAL_TEXT,
|
||||||
@@ -538,16 +221,11 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
reason,
|
reason,
|
||||||
}),
|
}),
|
||||||
isRunningMessageTurn: (chatJid) =>
|
isRunningMessageTurn: (chatJid) =>
|
||||||
deps.queue.getStatuses([chatJid])[0]?.runPhase ===
|
deps.queue.getStatuses([chatJid])[0]?.runPhase === 'running_messages',
|
||||||
'running_messages',
|
|
||||||
labelPairedSenders: labelPairedRuntimeSenders,
|
labelPairedSenders: labelPairedRuntimeSenders,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
},
|
||||||
logger.error({ err }, 'Error in message loop');
|
});
|
||||||
}
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, deps.pollInterval));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const recoverPendingMessages = (): void => {
|
const recoverPendingMessages = (): void => {
|
||||||
recoverRuntimePendingMessages({
|
recoverRuntimePendingMessages({
|
||||||
|
|||||||
Reference in New Issue
Block a user