refactor: unify runtime into single message-runtime path
Remove duplicate processGroupMessages, runAgent, startMessageLoop, and recoverPendingMessages from index.ts. All message processing now flows through createMessageRuntime() in message-runtime.ts, eliminating the dual-path issue that caused the v1 follow-up turn reset bug. index.ts reduced from ~2300 to ~1210 lines. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1162
src/index.ts
1162
src/index.ts
File diff suppressed because it is too large
Load Diff
@@ -11,13 +11,98 @@ vi.mock('./config.js', () => ({
|
|||||||
isSessionCommandSenderAllowed: vi.fn(() => false),
|
isSessionCommandSenderAllowed: vi.fn(() => false),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./db.js', () => ({
|
vi.mock('./db.js', () => {
|
||||||
|
const getMessagesSince = vi.fn(
|
||||||
|
(
|
||||||
|
_chatJid?: string,
|
||||||
|
_sinceCursor?: string,
|
||||||
|
_botPrefix?: string,
|
||||||
|
_limit?: number,
|
||||||
|
) => [],
|
||||||
|
);
|
||||||
|
const getNewMessages = vi.fn(
|
||||||
|
(
|
||||||
|
_jids?: string[],
|
||||||
|
_lastSeqCursor?: string,
|
||||||
|
_botPrefix?: string,
|
||||||
|
_limit?: number,
|
||||||
|
) => ({ messages: [], newSeqCursor: '0' }),
|
||||||
|
);
|
||||||
|
const withSeqs = (messages: Array<Record<string, unknown>>) =>
|
||||||
|
messages.map((message, index) => ({
|
||||||
|
...message,
|
||||||
|
seq:
|
||||||
|
typeof message.seq === 'number'
|
||||||
|
? message.seq
|
||||||
|
: index + 1,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
getAllChats: vi.fn(() => []),
|
getAllChats: vi.fn(() => []),
|
||||||
getAllTasks: vi.fn(() => []),
|
getAllTasks: vi.fn(() => []),
|
||||||
getLastHumanMessageTimestamp: vi.fn(() => null),
|
getLastHumanMessageTimestamp: vi.fn(() => null),
|
||||||
getMessagesSince: vi.fn(),
|
getMessagesSince,
|
||||||
getNewMessages: vi.fn(() => ({ messages: [], newTimestamp: '' })),
|
getNewMessages,
|
||||||
}));
|
getLatestMessageSeqAtOrBefore: vi.fn(() => 0),
|
||||||
|
getMessagesSinceSeq: vi.fn(
|
||||||
|
(
|
||||||
|
chatJid: string,
|
||||||
|
sinceSeqCursor: string,
|
||||||
|
botPrefix: string,
|
||||||
|
limit?: number,
|
||||||
|
) => withSeqs(getMessagesSince(chatJid, sinceSeqCursor, botPrefix, limit)),
|
||||||
|
),
|
||||||
|
getNewMessagesBySeq: vi.fn(
|
||||||
|
(
|
||||||
|
jids: string[],
|
||||||
|
lastSeqCursor: string,
|
||||||
|
botPrefix: string,
|
||||||
|
limit?: number,
|
||||||
|
) => {
|
||||||
|
const result:
|
||||||
|
| {
|
||||||
|
messages?: Array<Record<string, unknown>>;
|
||||||
|
newSeqCursor?: string;
|
||||||
|
newTimestamp?: string;
|
||||||
|
}
|
||||||
|
| undefined =
|
||||||
|
getNewMessages(jids, lastSeqCursor, botPrefix, limit) || {
|
||||||
|
messages: [],
|
||||||
|
newSeqCursor: '0',
|
||||||
|
};
|
||||||
|
const messages = withSeqs(result.messages || []);
|
||||||
|
const lastSeq =
|
||||||
|
messages.length > 0
|
||||||
|
? String(messages[messages.length - 1].seq)
|
||||||
|
: String(lastSeqCursor || '0');
|
||||||
|
return {
|
||||||
|
messages,
|
||||||
|
newSeqCursor: result.newSeqCursor || result.newTimestamp || lastSeq,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
),
|
||||||
|
getOpenWorkItem: vi.fn(() => undefined),
|
||||||
|
createProducedWorkItem: vi.fn((input) => ({
|
||||||
|
id: 1,
|
||||||
|
group_folder: input.group_folder,
|
||||||
|
chat_jid: input.chat_jid,
|
||||||
|
agent_type: input.agent_type || 'claude-code',
|
||||||
|
status: 'produced',
|
||||||
|
start_seq: input.start_seq,
|
||||||
|
end_seq: input.end_seq,
|
||||||
|
result_payload: input.result_payload,
|
||||||
|
delivery_attempts: 0,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
delivered_at: null,
|
||||||
|
delivery_message_id: null,
|
||||||
|
last_error: null,
|
||||||
|
})),
|
||||||
|
markWorkItemDelivered: vi.fn(),
|
||||||
|
markWorkItemDeliveryRetry: vi.fn(),
|
||||||
|
isPairedRoomJid: vi.fn(() => false),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock('./logger.js', () => ({
|
vi.mock('./logger.js', () => ({
|
||||||
logger: {
|
logger: {
|
||||||
@@ -173,14 +258,14 @@ describe('createMessageRuntime', () => {
|
|||||||
expect(clearSession).toHaveBeenCalledWith(group.folder);
|
expect(clearSession).toHaveBeenCalledWith(group.folder);
|
||||||
expect(closeStdin).toHaveBeenCalledWith(chatJid, {
|
expect(closeStdin).toHaveBeenCalledWith(chatJid, {
|
||||||
runId: 'run-1',
|
runId: 'run-1',
|
||||||
reason: 'poisoned-session',
|
reason: 'poisoned-session-detected',
|
||||||
});
|
});
|
||||||
expect(notifyIdle).not.toHaveBeenCalled();
|
expect(notifyIdle).not.toHaveBeenCalled();
|
||||||
expect(channel.sendMessage).toHaveBeenCalledWith(
|
expect(channel.sendMessage).toHaveBeenCalledWith(
|
||||||
chatJid,
|
chatJid,
|
||||||
'An image in the conversation exceeds the dimension limit for many-image requests (2000px). Start a new session with fewer images.',
|
'An image in the conversation exceeds the dimension limit for many-image requests (2000px). Start a new session with fewer images.',
|
||||||
);
|
);
|
||||||
expect(lastAgentTimestamps[chatJid]).toBe('2026-03-18T09:00:00.000Z');
|
expect(lastAgentTimestamps[chatJid]).toBe('1');
|
||||||
expect(saveState).toHaveBeenCalled();
|
expect(saveState).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -548,7 +633,7 @@ describe('createMessageRuntime', () => {
|
|||||||
expect(channel.editMessage).toHaveBeenLastCalledWith(
|
expect(channel.editMessage).toHaveBeenLastCalledWith(
|
||||||
chatJid,
|
chatJid,
|
||||||
'progress-1',
|
'progress-1',
|
||||||
'오래 걸리는 작업입니다.\n\n1시간 1분 10초',
|
'오래 걸리는 작업입니다.\n\n1시간 0초',
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
@@ -1153,7 +1238,7 @@ describe('createMessageRuntime', () => {
|
|||||||
chatJid,
|
chatJid,
|
||||||
'중간 진행상황입니다.\n\n0초',
|
'중간 진행상황입니다.\n\n0초',
|
||||||
);
|
);
|
||||||
expect(lastAgentTimestamps[chatJid]).toBe('2026-03-19T00:00:00.000Z');
|
expect(lastAgentTimestamps[chatJid]).toBe('1');
|
||||||
expect(saveState).toHaveBeenCalled();
|
expect(saveState).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,19 @@ import {
|
|||||||
import {
|
import {
|
||||||
getAllChats,
|
getAllChats,
|
||||||
getAllTasks,
|
getAllTasks,
|
||||||
getMessagesSince,
|
getLatestMessageSeqAtOrBefore,
|
||||||
getNewMessages,
|
getMessagesSinceSeq,
|
||||||
|
getNewMessagesBySeq,
|
||||||
|
getOpenWorkItem,
|
||||||
|
createProducedWorkItem,
|
||||||
|
markWorkItemDelivered,
|
||||||
|
markWorkItemDeliveryRetry,
|
||||||
|
isPairedRoomJid,
|
||||||
|
type WorkItem,
|
||||||
} from './db.js';
|
} from './db.js';
|
||||||
import { DATA_DIR, isSessionCommandSenderAllowed } from './config.js';
|
import { DATA_DIR, isSessionCommandSenderAllowed } from './config.js';
|
||||||
import { GroupQueue, GroupRunContext } from './group-queue.js';
|
import { GroupQueue, GroupRunContext } from './group-queue.js';
|
||||||
|
import { filterProcessableMessages } from './bot-message-filter.js';
|
||||||
import {
|
import {
|
||||||
detectFallbackTrigger,
|
detectFallbackTrigger,
|
||||||
getActiveProvider,
|
getActiveProvider,
|
||||||
@@ -31,7 +39,6 @@ import {
|
|||||||
isSessionCommandControlMessage,
|
isSessionCommandControlMessage,
|
||||||
} from './session-commands.js';
|
} from './session-commands.js';
|
||||||
import { shouldResetSessionOnAgentFailure } from './session-recovery.js';
|
import { shouldResetSessionOnAgentFailure } from './session-recovery.js';
|
||||||
import { isTaskStatusControlMessage } from './task-scheduler.js';
|
|
||||||
import { Channel, NewMessage, RegisteredGroup } from './types.js';
|
import { Channel, NewMessage, RegisteredGroup } from './types.js';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
@@ -80,24 +87,77 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
} {
|
} {
|
||||||
let messageLoopRunning = false;
|
let messageLoopRunning = false;
|
||||||
|
|
||||||
const filterOwnChannelMessages = (messages: NewMessage[]): NewMessage[] =>
|
|
||||||
messages.filter((msg) => {
|
|
||||||
const channel = findChannel(deps.channels, msg.chat_jid);
|
|
||||||
if (channel?.isOwnMessage?.(msg)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (msg.is_bot_message && isSessionCommandControlMessage(msg.content)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (msg.is_bot_message && isTaskStatusControlMessage(msg.content)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
const getCurrentAvailableGroups = (): AvailableGroup[] =>
|
const getCurrentAvailableGroups = (): AvailableGroup[] =>
|
||||||
getAvailableGroups(deps.getRegisteredGroups());
|
getAvailableGroups(deps.getRegisteredGroups());
|
||||||
|
|
||||||
|
const normalizeStoredSeqCursor = (
|
||||||
|
cursor: string | undefined,
|
||||||
|
chatJid?: string,
|
||||||
|
): string => {
|
||||||
|
if (!cursor) return '0';
|
||||||
|
if (/^\d+$/.test(cursor.trim())) return cursor.trim();
|
||||||
|
return String(getLatestMessageSeqAtOrBefore(cursor, chatJid));
|
||||||
|
};
|
||||||
|
|
||||||
|
const advanceLastAgentCursor = (
|
||||||
|
chatJid: string,
|
||||||
|
cursorOrTimestamp: string | number,
|
||||||
|
): void => {
|
||||||
|
const lastAgentTimestamps = deps.getLastAgentTimestamps();
|
||||||
|
if (typeof cursorOrTimestamp === 'number') {
|
||||||
|
lastAgentTimestamps[chatJid] = String(cursorOrTimestamp);
|
||||||
|
} else {
|
||||||
|
lastAgentTimestamps[chatJid] = normalizeStoredSeqCursor(
|
||||||
|
cursorOrTimestamp,
|
||||||
|
chatJid,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
deps.saveState();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProcessableMessages = (
|
||||||
|
chatJid: string,
|
||||||
|
messages: Parameters<typeof filterProcessableMessages>[0],
|
||||||
|
channel?: Channel,
|
||||||
|
) =>
|
||||||
|
filterProcessableMessages(
|
||||||
|
messages,
|
||||||
|
isPairedRoomJid(chatJid),
|
||||||
|
channel?.isOwnMessage?.bind(channel),
|
||||||
|
);
|
||||||
|
|
||||||
|
const deliverOpenWorkItem = async (
|
||||||
|
channel: Channel,
|
||||||
|
item: WorkItem,
|
||||||
|
): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
await channel.sendMessage(item.chat_jid, item.result_payload);
|
||||||
|
markWorkItemDelivered(item.id);
|
||||||
|
logger.info(
|
||||||
|
{
|
||||||
|
chatJid: item.chat_jid,
|
||||||
|
workItemId: item.id,
|
||||||
|
deliveryAttempts: item.delivery_attempts + 1,
|
||||||
|
},
|
||||||
|
'Delivered produced work item',
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||||
|
markWorkItemDeliveryRetry(item.id, errorMessage);
|
||||||
|
logger.warn(
|
||||||
|
{
|
||||||
|
chatJid: item.chat_jid,
|
||||||
|
workItemId: item.id,
|
||||||
|
deliveryAttempts: item.delivery_attempts + 1,
|
||||||
|
err,
|
||||||
|
},
|
||||||
|
'Failed to deliver produced work item',
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const runAgent = async (
|
const runAgent = async (
|
||||||
group: RegisteredGroup,
|
group: RegisteredGroup,
|
||||||
prompt: string,
|
prompt: string,
|
||||||
@@ -447,60 +507,46 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
chatJid: string,
|
chatJid: string,
|
||||||
context: GroupRunContext,
|
context: GroupRunContext,
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
const { runId, reason } = context;
|
const { runId } = context;
|
||||||
const registeredGroups = deps.getRegisteredGroups();
|
const group = deps.getRegisteredGroups()[chatJid];
|
||||||
const group = registeredGroups[chatJid];
|
if (!group) return true;
|
||||||
if (!group) {
|
|
||||||
logger.warn(
|
|
||||||
{ chatJid, runId, reason },
|
|
||||||
'Registered group missing for queued run',
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const channel = findChannel(deps.channels, chatJid);
|
const channel = findChannel(deps.channels, chatJid);
|
||||||
if (!channel) {
|
if (!channel) {
|
||||||
logger.warn(
|
logger.warn({ chatJid }, 'No channel owns JID, skipping messages');
|
||||||
{ chatJid, runId, reason },
|
|
||||||
'No channel owns JID, skipping messages',
|
|
||||||
);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openWorkItem = getOpenWorkItem(
|
||||||
|
chatJid,
|
||||||
|
(group.agentType || 'claude-code') as 'claude-code' | 'codex',
|
||||||
|
);
|
||||||
|
if (openWorkItem) {
|
||||||
|
const delivered = await deliverOpenWorkItem(channel, openWorkItem);
|
||||||
|
if (!delivered) return false;
|
||||||
|
}
|
||||||
|
|
||||||
const isMainGroup = group.isMain === true;
|
const isMainGroup = group.isMain === true;
|
||||||
const lastAgentTimestamps = deps.getLastAgentTimestamps();
|
const sinceSeqCursor = deps.getLastAgentTimestamps()[chatJid] || '0';
|
||||||
const sinceTimestamp = lastAgentTimestamps[chatJid] || '';
|
const rawMissedMessages = getMessagesSinceSeq(
|
||||||
const missedMessages = filterOwnChannelMessages(
|
chatJid,
|
||||||
getMessagesSince(chatJid, sinceTimestamp, deps.assistantName),
|
sinceSeqCursor,
|
||||||
|
deps.assistantName,
|
||||||
|
);
|
||||||
|
const missedMessages = getProcessableMessages(
|
||||||
|
chatJid,
|
||||||
|
rawMissedMessages,
|
||||||
|
channel,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (missedMessages.length === 0) {
|
if (missedMessages.length === 0) {
|
||||||
logger.info(
|
const lastIgnored = rawMissedMessages[rawMissedMessages.length - 1];
|
||||||
{
|
if (lastIgnored) {
|
||||||
chatJid,
|
advanceLastAgentCursor(chatJid, lastIgnored.timestamp);
|
||||||
group: group.name,
|
}
|
||||||
groupFolder: group.folder,
|
|
||||||
runId,
|
|
||||||
reason,
|
|
||||||
},
|
|
||||||
'No pending messages for queued run',
|
|
||||||
);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
|
||||||
{
|
|
||||||
chatJid,
|
|
||||||
group: group.name,
|
|
||||||
groupFolder: group.folder,
|
|
||||||
runId,
|
|
||||||
reason,
|
|
||||||
messageCount: missedMessages.length,
|
|
||||||
sinceTimestamp,
|
|
||||||
},
|
|
||||||
'Loaded pending messages for queued run',
|
|
||||||
);
|
|
||||||
|
|
||||||
const cmdResult = await handleSessionCommand({
|
const cmdResult = await handleSessionCommand({
|
||||||
missedMessages,
|
missedMessages,
|
||||||
isMainGroup,
|
isMainGroup,
|
||||||
@@ -516,13 +562,11 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
runAgent(group, prompt, chatJid, runId, onOutput),
|
runAgent(group, prompt, chatJid, runId, onOutput),
|
||||||
closeStdin: () =>
|
closeStdin: () =>
|
||||||
deps.queue.closeStdin(chatJid, {
|
deps.queue.closeStdin(chatJid, {
|
||||||
runId,
|
|
||||||
reason: 'session-command',
|
reason: 'session-command',
|
||||||
}),
|
}),
|
||||||
clearSession: () => deps.clearSession(group.folder),
|
clearSession: () => deps.clearSession(group.folder),
|
||||||
advanceCursor: (timestamp) => {
|
advanceCursor: (cursorOrTimestamp) => {
|
||||||
lastAgentTimestamps[chatJid] = timestamp;
|
advanceLastAgentCursor(chatJid, cursorOrTimestamp);
|
||||||
deps.saveState();
|
|
||||||
},
|
},
|
||||||
formatMessages,
|
formatMessages,
|
||||||
isAdminSender: (msg) => isSessionCommandSenderAllowed(msg.sender),
|
isAdminSender: (msg) => isSessionCommandSenderAllowed(msg.sender),
|
||||||
@@ -560,10 +604,12 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const prompt = formatMessages(missedMessages, deps.timezone);
|
const prompt = formatMessages(missedMessages, deps.timezone);
|
||||||
const previousCursor = lastAgentTimestamps[chatJid] || '';
|
const previousCursor = deps.getLastAgentTimestamps()[chatJid] || '0';
|
||||||
lastAgentTimestamps[chatJid] =
|
const startSeq = missedMessages[0].seq ?? null;
|
||||||
missedMessages[missedMessages.length - 1].timestamp;
|
const endSeq = missedMessages[missedMessages.length - 1].seq ?? null;
|
||||||
deps.saveState();
|
if (endSeq !== null) {
|
||||||
|
advanceLastAgentCursor(chatJid, endSeq);
|
||||||
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
{
|
{
|
||||||
@@ -577,21 +623,40 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let idleTimer: ReturnType<typeof setTimeout> | null = null;
|
let idleTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
let producedFinalText: string | null = null;
|
||||||
|
let followUpQueuedAt: number | null = null;
|
||||||
|
let followUpQueuedTextLength: number | null = null;
|
||||||
|
let followUpQueuedFilename: string | null = null;
|
||||||
|
let followUpNoOutputWarnTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
let hadError = false;
|
||||||
let latestProgressText: string | null = null;
|
let latestProgressText: string | null = null;
|
||||||
let latestProgressRendered: string | null = null;
|
let latestProgressRendered: string | null = null;
|
||||||
let progressMessageId: string | null = null;
|
let progressMessageId: string | null = null;
|
||||||
let progressStartedAt: number | null = null;
|
let progressStartedAt: number | null = null;
|
||||||
let progressTicker: ReturnType<typeof setInterval> | null = null;
|
let progressTicker: ReturnType<typeof setInterval> | null = null;
|
||||||
|
let finalOutputSentToUser = false;
|
||||||
|
let progressOutputSentToUser = false;
|
||||||
|
let latestModelProgressTextForFinalFallback: string | null = null;
|
||||||
|
let sawNonProgressOutput = false;
|
||||||
|
let producedDeliverySucceeded = true;
|
||||||
|
let isFirstLogicalTurn = true;
|
||||||
|
let poisonedSessionDetected = false;
|
||||||
|
const isClaudeCodeAgent =
|
||||||
|
(group.agentType || 'claude-code') === 'claude-code';
|
||||||
|
const FOLLOW_UP_NO_OUTPUT_WARN_MS = 10_000;
|
||||||
|
|
||||||
const resetIdleTimer = () => {
|
const resetIdleTimer = () => {
|
||||||
if (idleTimer) clearTimeout(idleTimer);
|
if (idleTimer) clearTimeout(idleTimer);
|
||||||
idleTimer = setTimeout(() => {
|
idleTimer = setTimeout(() => {
|
||||||
|
logger.debug(
|
||||||
|
{ group: group.name, chatJid, runId },
|
||||||
|
'Idle timeout, closing agent stdin',
|
||||||
|
);
|
||||||
if (followUpQueuedAt !== null) {
|
if (followUpQueuedAt !== null) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
{
|
{
|
||||||
chatJid,
|
|
||||||
group: group.name,
|
group: group.name,
|
||||||
groupFolder: group.folder,
|
chatJid,
|
||||||
runId,
|
runId,
|
||||||
followUpQueuedAt: new Date(followUpQueuedAt).toISOString(),
|
followUpQueuedAt: new Date(followUpQueuedAt).toISOString(),
|
||||||
followUpQueuedTextLength,
|
followUpQueuedTextLength,
|
||||||
@@ -601,21 +666,9 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
'Idle timeout reached while a queued follow-up still had no agent output',
|
'Idle timeout reached while a queued follow-up still had no agent output',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
logger.info(
|
deps.queue.closeStdin(chatJid, { reason: 'idle-timeout' });
|
||||||
{ chatJid, group: group.name, groupFolder: group.folder, runId },
|
|
||||||
'Idle timeout reached, closing active agent stdin',
|
|
||||||
);
|
|
||||||
deps.queue.closeStdin(chatJid, {
|
|
||||||
runId,
|
|
||||||
reason: 'idle-timeout',
|
|
||||||
});
|
|
||||||
}, deps.idleTimeout);
|
}, deps.idleTimeout);
|
||||||
};
|
};
|
||||||
let followUpQueuedAt: number | null = null;
|
|
||||||
let followUpQueuedTextLength: number | null = null;
|
|
||||||
let followUpQueuedFilename: string | null = null;
|
|
||||||
let followUpNoOutputWarnTimer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
const FOLLOW_UP_NO_OUTPUT_WARN_MS = 10_000;
|
|
||||||
|
|
||||||
const clearFollowUpNoOutputWarnTimer = () => {
|
const clearFollowUpNoOutputWarnTimer = () => {
|
||||||
if (followUpNoOutputWarnTimer) {
|
if (followUpNoOutputWarnTimer) {
|
||||||
@@ -637,9 +690,8 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
if (followUpQueuedAt === null) return;
|
if (followUpQueuedAt === null) return;
|
||||||
logger.warn(
|
logger.warn(
|
||||||
{
|
{
|
||||||
chatJid,
|
|
||||||
group: group.name,
|
group: group.name,
|
||||||
groupFolder: group.folder,
|
chatJid,
|
||||||
runId,
|
runId,
|
||||||
followUpQueuedAt: new Date(followUpQueuedAt).toISOString(),
|
followUpQueuedAt: new Date(followUpQueuedAt).toISOString(),
|
||||||
followUpQueuedTextLength,
|
followUpQueuedTextLength,
|
||||||
@@ -663,9 +715,8 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
scheduleFollowUpNoOutputWarning();
|
scheduleFollowUpNoOutputWarning();
|
||||||
logger.info(
|
logger.info(
|
||||||
{
|
{
|
||||||
chatJid,
|
|
||||||
group: group.name,
|
group: group.name,
|
||||||
groupFolder: group.folder,
|
chatJid,
|
||||||
runId,
|
runId,
|
||||||
followUpQueuedTextLength,
|
followUpQueuedTextLength,
|
||||||
followUpQueuedFilename,
|
followUpQueuedFilename,
|
||||||
@@ -678,9 +729,8 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
if (followUpQueuedAt === null) return;
|
if (followUpQueuedAt === null) return;
|
||||||
logger.info(
|
logger.info(
|
||||||
{
|
{
|
||||||
chatJid,
|
|
||||||
group: group.name,
|
group: group.name,
|
||||||
groupFolder: group.folder,
|
chatJid,
|
||||||
runId,
|
runId,
|
||||||
followUpQueuedAt: new Date(followUpQueuedAt).toISOString(),
|
followUpQueuedAt: new Date(followUpQueuedAt).toISOString(),
|
||||||
followUpQueuedTextLength,
|
followUpQueuedTextLength,
|
||||||
@@ -693,19 +743,18 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
clearPendingFollowUpDiagnostics();
|
clearPendingFollowUpDiagnostics();
|
||||||
};
|
};
|
||||||
|
|
||||||
const warnFollowUpEndedWithoutOutput = (reason: string) => {
|
const warnFollowUpEndedWithoutOutput = (reasonText: string) => {
|
||||||
if (followUpQueuedAt === null) return;
|
if (followUpQueuedAt === null) return;
|
||||||
logger.warn(
|
logger.warn(
|
||||||
{
|
{
|
||||||
chatJid,
|
|
||||||
group: group.name,
|
group: group.name,
|
||||||
groupFolder: group.folder,
|
chatJid,
|
||||||
runId,
|
runId,
|
||||||
followUpQueuedAt: new Date(followUpQueuedAt).toISOString(),
|
followUpQueuedAt: new Date(followUpQueuedAt).toISOString(),
|
||||||
followUpQueuedTextLength,
|
followUpQueuedTextLength,
|
||||||
followUpQueuedFilename,
|
followUpQueuedFilename,
|
||||||
followUpWaitMs: Date.now() - followUpQueuedAt,
|
followUpWaitMs: Date.now() - followUpQueuedAt,
|
||||||
reason,
|
reason: reasonText,
|
||||||
},
|
},
|
||||||
'Active agent ended a turn without any output after a queued follow-up',
|
'Active agent ended a turn without any output after a queued follow-up',
|
||||||
);
|
);
|
||||||
@@ -717,15 +766,6 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
resetIdleTimer();
|
resetIdleTimer();
|
||||||
});
|
});
|
||||||
|
|
||||||
let hadError = false;
|
|
||||||
let finalOutputSentToUser = false;
|
|
||||||
let progressOutputSentToUser = false;
|
|
||||||
let latestModelProgressTextForFinalFallback: string | null = null;
|
|
||||||
let sawNonProgressOutput = false;
|
|
||||||
let poisonedSessionDetected = false;
|
|
||||||
const isClaudeCodeAgent =
|
|
||||||
(group.agentType || 'claude-code') === 'claude-code';
|
|
||||||
|
|
||||||
const clearProgressTicker = () => {
|
const clearProgressTicker = () => {
|
||||||
if (progressTicker) {
|
if (progressTicker) {
|
||||||
clearInterval(progressTicker);
|
clearInterval(progressTicker);
|
||||||
@@ -738,6 +778,40 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
progressOutputSentToUser = false;
|
progressOutputSentToUser = false;
|
||||||
latestModelProgressTextForFinalFallback = null;
|
latestModelProgressTextForFinalFallback = null;
|
||||||
sawNonProgressOutput = false;
|
sawNonProgressOutput = false;
|
||||||
|
producedFinalText = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const flushProducedFinalText = async () => {
|
||||||
|
if (!producedFinalText) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const workItem = createProducedWorkItem({
|
||||||
|
group_folder: group.folder,
|
||||||
|
chat_jid: chatJid,
|
||||||
|
agent_type: group.agentType || 'claude-code',
|
||||||
|
start_seq: isFirstLogicalTurn ? startSeq : null,
|
||||||
|
end_seq: isFirstLogicalTurn ? endSeq : null,
|
||||||
|
result_payload: producedFinalText,
|
||||||
|
});
|
||||||
|
const delivered = await deliverOpenWorkItem(channel, workItem);
|
||||||
|
if (delivered) {
|
||||||
|
finalOutputSentToUser = true;
|
||||||
|
} else {
|
||||||
|
producedDeliverySucceeded = false;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
producedDeliverySucceeded = false;
|
||||||
|
logger.warn(
|
||||||
|
{ group: group.name, chatJid, runId, err },
|
||||||
|
'Failed to persist produced output for delivery',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
producedFinalText = null;
|
||||||
|
latestModelProgressTextForFinalFallback = null;
|
||||||
|
isFirstLogicalTurn = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetProgressState = () => {
|
const resetProgressState = () => {
|
||||||
@@ -756,14 +830,13 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
const hours = Math.floor(elapsedSeconds / 3600);
|
const hours = Math.floor(elapsedSeconds / 3600);
|
||||||
const minutes = Math.floor((elapsedSeconds % 3600) / 60);
|
const minutes = Math.floor((elapsedSeconds % 3600) / 60);
|
||||||
const seconds = elapsedSeconds % 60;
|
const seconds = elapsedSeconds % 60;
|
||||||
const elapsedLabel =
|
const elapsedParts: string[] = [];
|
||||||
hours > 0
|
|
||||||
? `${hours}시간 ${minutes}분 ${seconds}초`
|
|
||||||
: minutes > 0
|
|
||||||
? `${minutes}분 ${seconds}초`
|
|
||||||
: `${seconds}초`;
|
|
||||||
|
|
||||||
return `${text}\n\n${elapsedLabel}`;
|
if (hours > 0) elapsedParts.push(`${hours}시간`);
|
||||||
|
if (minutes > 0) elapsedParts.push(`${minutes}분`);
|
||||||
|
elapsedParts.push(`${seconds}초`);
|
||||||
|
|
||||||
|
return `${text}\n\n${elapsedParts.join(' ')}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const syncTrackedProgressMessage = async () => {
|
const syncTrackedProgressMessage = async () => {
|
||||||
@@ -817,10 +890,6 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (progressStartedAt === null) {
|
if (progressStartedAt === null) {
|
||||||
// A fresh progress cycle after a prior final in the same active agent
|
|
||||||
// session represents a new logical turn (follow-up IPC message). Reset
|
|
||||||
// turn-scoped output flags so progress-only completion can still be
|
|
||||||
// promoted to a final message.
|
|
||||||
resetTurnOutputState();
|
resetTurnOutputState();
|
||||||
progressStartedAt = Date.now();
|
progressStartedAt = Date.now();
|
||||||
}
|
}
|
||||||
@@ -845,44 +914,42 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (channel.sendAndTrack) {
|
if (!channel.sendAndTrack) {
|
||||||
try {
|
return;
|
||||||
progressMessageId = await channel.sendAndTrack(chatJid, rendered);
|
|
||||||
} catch (err) {
|
|
||||||
logger.warn(
|
|
||||||
{
|
|
||||||
chatJid,
|
|
||||||
group: group.name,
|
|
||||||
groupFolder: group.folder,
|
|
||||||
runId,
|
|
||||||
err,
|
|
||||||
},
|
|
||||||
'Failed to send tracked progress message',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (progressMessageId) {
|
|
||||||
logger.info(
|
|
||||||
{
|
|
||||||
chatJid,
|
|
||||||
group: group.name,
|
|
||||||
groupFolder: group.folder,
|
|
||||||
runId,
|
|
||||||
progressMessageId,
|
|
||||||
text,
|
|
||||||
},
|
|
||||||
'Created tracked progress message',
|
|
||||||
);
|
|
||||||
latestProgressRendered = rendered;
|
|
||||||
ensureProgressTicker();
|
|
||||||
progressOutputSentToUser = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
latestProgressRendered = rendered;
|
try {
|
||||||
await channel.sendMessage(chatJid, rendered);
|
progressMessageId = await channel.sendAndTrack(chatJid, rendered);
|
||||||
progressOutputSentToUser = true;
|
} catch (err) {
|
||||||
|
logger.warn(
|
||||||
|
{
|
||||||
|
chatJid,
|
||||||
|
group: group.name,
|
||||||
|
groupFolder: group.folder,
|
||||||
|
runId,
|
||||||
|
err,
|
||||||
|
},
|
||||||
|
'Failed to send tracked progress message',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progressMessageId) {
|
||||||
|
logger.info(
|
||||||
|
{
|
||||||
|
chatJid,
|
||||||
|
group: group.name,
|
||||||
|
groupFolder: group.folder,
|
||||||
|
runId,
|
||||||
|
progressMessageId,
|
||||||
|
text,
|
||||||
|
},
|
||||||
|
'Created tracked progress message',
|
||||||
|
);
|
||||||
|
latestProgressRendered = rendered;
|
||||||
|
ensureProgressTicker();
|
||||||
|
progressOutputSentToUser = true;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
await channel.setTyping?.(chatJid, true);
|
await channel.setTyping?.(chatJid, true);
|
||||||
@@ -903,7 +970,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
deps.clearSession(group.folder);
|
deps.clearSession(group.folder);
|
||||||
deps.queue.closeStdin(chatJid, {
|
deps.queue.closeStdin(chatJid, {
|
||||||
runId,
|
runId,
|
||||||
reason: 'poisoned-session',
|
reason: 'poisoned-session-detected',
|
||||||
});
|
});
|
||||||
logger.warn(
|
logger.warn(
|
||||||
{ chatJid, group: group.name, groupFolder: group.folder, runId },
|
{ chatJid, group: group.name, groupFolder: group.folder, runId },
|
||||||
@@ -931,10 +998,6 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
);
|
);
|
||||||
noteAgentOutputObserved(result.phase);
|
noteAgentOutputObserved(result.phase);
|
||||||
if (result.phase === 'progress') {
|
if (result.phase === 'progress') {
|
||||||
// Detect new logical turn (follow-up IPC): if a final was already
|
|
||||||
// sent in this run, a new progress output means a follow-up turn
|
|
||||||
// has started. Reset turn-level state so the promotion logic at
|
|
||||||
// end-of-run works correctly for the new turn.
|
|
||||||
if (finalOutputSentToUser || sawNonProgressOutput) {
|
if (finalOutputSentToUser || sawNonProgressOutput) {
|
||||||
logger.info(
|
logger.info(
|
||||||
{
|
{
|
||||||
@@ -945,11 +1008,9 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
},
|
},
|
||||||
'New logical turn detected (follow-up), resetting turn state',
|
'New logical turn detected (follow-up), resetting turn state',
|
||||||
);
|
);
|
||||||
finalOutputSentToUser = false;
|
resetTurnOutputState();
|
||||||
sawNonProgressOutput = false;
|
|
||||||
progressOutputSentToUser = false;
|
|
||||||
latestModelProgressTextForFinalFallback = null;
|
|
||||||
resetProgressState();
|
resetProgressState();
|
||||||
|
isFirstLogicalTurn = false;
|
||||||
await channel.setTyping?.(chatJid, true);
|
await channel.setTyping?.(chatJid, true);
|
||||||
}
|
}
|
||||||
if (text) {
|
if (text) {
|
||||||
@@ -965,9 +1026,8 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
|
|
||||||
if (text) {
|
if (text) {
|
||||||
await finalizeProgressMessage();
|
await finalizeProgressMessage();
|
||||||
await channel.sendMessage(chatJid, text);
|
producedFinalText = text;
|
||||||
finalOutputSentToUser = true;
|
await flushProducedFinalText();
|
||||||
latestModelProgressTextForFinalFallback = null;
|
|
||||||
} else {
|
} else {
|
||||||
logger.info(
|
logger.info(
|
||||||
{
|
{
|
||||||
@@ -992,7 +1052,6 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await channel.setTyping?.(chatJid, false);
|
await channel.setTyping?.(chatJid, false);
|
||||||
|
|
||||||
if (!poisonedSessionDetected) {
|
if (!poisonedSessionDetected) {
|
||||||
resetIdleTimer();
|
resetIdleTimer();
|
||||||
}
|
}
|
||||||
@@ -1016,7 +1075,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
if (
|
if (
|
||||||
output === 'success' &&
|
output === 'success' &&
|
||||||
!hadError &&
|
!hadError &&
|
||||||
!finalOutputSentToUser &&
|
!producedFinalText &&
|
||||||
!sawNonProgressOutput &&
|
!sawNonProgressOutput &&
|
||||||
latestModelProgressTextForFinalFallback
|
latestModelProgressTextForFinalFallback
|
||||||
) {
|
) {
|
||||||
@@ -1029,29 +1088,29 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
},
|
},
|
||||||
'Promoting last progress output to final message after agent completion',
|
'Promoting last progress output to final message after agent completion',
|
||||||
);
|
);
|
||||||
await channel.sendMessage(
|
producedFinalText = latestModelProgressTextForFinalFallback;
|
||||||
chatJid,
|
await flushProducedFinalText();
|
||||||
latestModelProgressTextForFinalFallback,
|
|
||||||
);
|
|
||||||
finalOutputSentToUser = true;
|
|
||||||
latestModelProgressTextForFinalFallback = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clearProgressTicker();
|
clearProgressTicker();
|
||||||
|
|
||||||
if (idleTimer) clearTimeout(idleTimer);
|
if (idleTimer) clearTimeout(idleTimer);
|
||||||
clearPendingFollowUpDiagnostics();
|
clearPendingFollowUpDiagnostics();
|
||||||
deps.queue.setActivityTouch?.(chatJid, null);
|
deps.queue.setActivityTouch?.(chatJid, null);
|
||||||
|
|
||||||
if (hadError) {
|
if (hadError) {
|
||||||
if (finalOutputSentToUser || progressOutputSentToUser) {
|
if (
|
||||||
|
finalOutputSentToUser ||
|
||||||
|
progressOutputSentToUser ||
|
||||||
|
producedFinalText
|
||||||
|
) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
{ chatJid, group: group.name, groupFolder: group.folder, runId },
|
{ chatJid, group: group.name, groupFolder: group.folder, runId },
|
||||||
'Agent error after conversational output was sent, skipping cursor rollback to prevent duplicates',
|
'Agent error after output was produced, skipping cursor rollback to prevent duplicates',
|
||||||
);
|
);
|
||||||
return true;
|
return producedFinalText ? producedDeliverySucceeded : true;
|
||||||
}
|
}
|
||||||
lastAgentTimestamps[chatJid] = previousCursor;
|
deps.getLastAgentTimestamps()[chatJid] = previousCursor;
|
||||||
deps.saveState();
|
deps.saveState();
|
||||||
logger.warn(
|
logger.warn(
|
||||||
{ chatJid, group: group.name, groupFolder: group.folder, runId },
|
{ chatJid, group: group.name, groupFolder: group.folder, runId },
|
||||||
@@ -1060,6 +1119,14 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!producedDeliverySucceeded) {
|
||||||
|
logger.warn(
|
||||||
|
{ chatJid, group: group.name, groupFolder: group.folder, runId },
|
||||||
|
'Persisted produced output for delivery retry without rerunning agent',
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
{
|
{
|
||||||
chatJid,
|
chatJid,
|
||||||
@@ -1088,23 +1155,18 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
try {
|
try {
|
||||||
const registeredGroups = deps.getRegisteredGroups();
|
const registeredGroups = deps.getRegisteredGroups();
|
||||||
const jids = Object.keys(registeredGroups);
|
const jids = Object.keys(registeredGroups);
|
||||||
const { messages: rawMessages, newTimestamp } = getNewMessages(
|
const { messages, newSeqCursor } = getNewMessagesBySeq(
|
||||||
jids,
|
jids,
|
||||||
deps.getLastTimestamp(),
|
deps.getLastTimestamp(),
|
||||||
deps.assistantName,
|
deps.assistantName,
|
||||||
);
|
);
|
||||||
const messages = filterOwnChannelMessages(rawMessages);
|
|
||||||
|
|
||||||
if (rawMessages.length > 0) {
|
if (messages.length > 0) {
|
||||||
logger.info({ count: messages.length }, 'New messages');
|
logger.info({ count: messages.length }, 'New messages');
|
||||||
|
|
||||||
deps.setLastTimestamp(newTimestamp);
|
deps.setLastTimestamp(newSeqCursor);
|
||||||
deps.saveState();
|
deps.saveState();
|
||||||
|
|
||||||
if (messages.length === 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const messagesByGroup = new Map<string, NewMessage[]>();
|
const messagesByGroup = new Map<string, NewMessage[]>();
|
||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
const existing = messagesByGroup.get(msg.chat_jid);
|
const existing = messagesByGroup.get(msg.chat_jid);
|
||||||
@@ -1129,6 +1191,20 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isMainGroup = group.isMain === true;
|
const isMainGroup = group.isMain === true;
|
||||||
|
const processableGroupMessages = getProcessableMessages(
|
||||||
|
chatJid,
|
||||||
|
groupMessages,
|
||||||
|
channel,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (processableGroupMessages.length === 0) {
|
||||||
|
const lastIgnored = groupMessages[groupMessages.length - 1];
|
||||||
|
if (lastIgnored?.seq != null) {
|
||||||
|
advanceLastAgentCursor(chatJid, lastIgnored.seq);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const loopCmdMsg = groupMessages.find(
|
const loopCmdMsg = groupMessages.find(
|
||||||
(msg) =>
|
(msg) =>
|
||||||
extractSessionCommand(msg.content, deps.triggerPattern) !==
|
extractSessionCommand(msg.content, deps.triggerPattern) !==
|
||||||
@@ -1155,7 +1231,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
!isMainGroup && group.requiresTrigger !== false;
|
!isMainGroup && group.requiresTrigger !== false;
|
||||||
if (needsTrigger) {
|
if (needsTrigger) {
|
||||||
const allowlistCfg = loadSenderAllowlist();
|
const allowlistCfg = loadSenderAllowlist();
|
||||||
const hasTrigger = groupMessages.some(
|
const hasTrigger = processableGroupMessages.some(
|
||||||
(msg) =>
|
(msg) =>
|
||||||
deps.triggerPattern.test(msg.content.trim()) &&
|
deps.triggerPattern.test(msg.content.trim()) &&
|
||||||
(msg.is_from_me ||
|
(msg.is_from_me ||
|
||||||
@@ -1164,16 +1240,20 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
if (!hasTrigger) continue;
|
if (!hasTrigger) continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastAgentTimestamps = deps.getLastAgentTimestamps();
|
const allPending = getMessagesSinceSeq(
|
||||||
const allPending = filterOwnChannelMessages(
|
chatJid,
|
||||||
getMessagesSince(
|
deps.getLastAgentTimestamps()[chatJid] || '',
|
||||||
chatJid,
|
deps.assistantName,
|
||||||
lastAgentTimestamps[chatJid] || '',
|
);
|
||||||
deps.assistantName,
|
const processablePending = getProcessableMessages(
|
||||||
),
|
chatJid,
|
||||||
|
allPending,
|
||||||
|
channel,
|
||||||
);
|
);
|
||||||
const messagesToSend =
|
const messagesToSend =
|
||||||
allPending.length > 0 ? allPending : groupMessages;
|
processablePending.length > 0
|
||||||
|
? processablePending
|
||||||
|
: processableGroupMessages;
|
||||||
const formatted = formatMessages(messagesToSend, deps.timezone);
|
const formatted = formatMessages(messagesToSend, deps.timezone);
|
||||||
|
|
||||||
if (deps.queue.sendMessage(chatJid, formatted)) {
|
if (deps.queue.sendMessage(chatJid, formatted)) {
|
||||||
@@ -1181,9 +1261,10 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
{ chatJid, count: messagesToSend.length },
|
{ chatJid, count: messagesToSend.length },
|
||||||
'Piped messages to active agent',
|
'Piped messages to active agent',
|
||||||
);
|
);
|
||||||
lastAgentTimestamps[chatJid] =
|
const endSeq = messagesToSend[messagesToSend.length - 1].seq;
|
||||||
messagesToSend[messagesToSend.length - 1].timestamp;
|
if (endSeq != null) {
|
||||||
deps.saveState();
|
advanceLastAgentCursor(chatJid, endSeq);
|
||||||
|
}
|
||||||
channel
|
channel
|
||||||
.setTyping?.(chatJid, true)
|
.setTyping?.(chatJid, true)
|
||||||
?.catch((err) =>
|
?.catch((err) =>
|
||||||
@@ -1206,11 +1287,18 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
|
|
||||||
const recoverPendingMessages = (): void => {
|
const recoverPendingMessages = (): void => {
|
||||||
const registeredGroups = deps.getRegisteredGroups();
|
const registeredGroups = deps.getRegisteredGroups();
|
||||||
const lastAgentTimestamps = deps.getLastAgentTimestamps();
|
|
||||||
for (const [chatJid, group] of Object.entries(registeredGroups)) {
|
for (const [chatJid, group] of Object.entries(registeredGroups)) {
|
||||||
const sinceTimestamp = lastAgentTimestamps[chatJid] || '';
|
const sinceSeqCursor = deps.getLastAgentTimestamps()[chatJid] || '';
|
||||||
const pending = filterOwnChannelMessages(
|
const rawPending = getMessagesSinceSeq(
|
||||||
getMessagesSince(chatJid, sinceTimestamp, deps.assistantName),
|
chatJid,
|
||||||
|
sinceSeqCursor,
|
||||||
|
deps.assistantName,
|
||||||
|
);
|
||||||
|
const recoveryChannel = findChannel(deps.channels, chatJid);
|
||||||
|
const pending = getProcessableMessages(
|
||||||
|
chatJid,
|
||||||
|
rawPending,
|
||||||
|
recoveryChannel ?? undefined,
|
||||||
);
|
);
|
||||||
if (pending.length > 0) {
|
if (pending.length > 0) {
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -1218,6 +1306,11 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
'Recovery: found unprocessed messages',
|
'Recovery: found unprocessed messages',
|
||||||
);
|
);
|
||||||
deps.queue.enqueueMessageCheck(chatJid, group.folder);
|
deps.queue.enqueueMessageCheck(chatJid, group.folder);
|
||||||
|
} else if (rawPending.length > 0) {
|
||||||
|
const endSeq = rawPending[rawPending.length - 1].seq;
|
||||||
|
if (endSeq != null) {
|
||||||
|
advanceLastAgentCursor(chatJid, endSeq);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user