fix: prevent full message history from being sent to container agents

When lastAgentTimestamp was missing (new group, corrupted state, or
startup recovery), the empty-string fallback caused getMessagesSince to
return up to 200 messages — the entire group history. This sent a
massive prompt to the container agent instead of just recent messages.

Fix: recover the cursor from the last bot reply timestamp in the DB
(proof of what we already processed), and cap all prompt queries to a
configurable MAX_MESSAGES_PER_PROMPT (default 10). Covers all three
call sites: processGroupMessages, the piping path, and
recoverPendingMessages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
exe.dev user
2026-03-27 18:25:46 +00:00
parent a41746530f
commit c98205ca0d
4 changed files with 138 additions and 5 deletions

View File

@@ -9,6 +9,7 @@ import {
getTriggerPattern,
GROUPS_DIR,
IDLE_TIMEOUT,
MAX_MESSAGES_PER_PROMPT,
ONECLI_URL,
POLL_INTERVAL,
TIMEZONE,
@@ -33,6 +34,7 @@ import {
getAllRegisteredGroups,
getAllSessions,
getAllTasks,
getLastBotMessageTimestamp,
getMessagesSince,
getNewMessages,
getRouterState,
@@ -112,6 +114,27 @@ function loadState(): void {
);
}
/**
* Return the message cursor for a group, recovering from the last bot reply
* if lastAgentTimestamp is missing (new group, corrupted state, restart).
*/
function getOrRecoverCursor(chatJid: string): string {
const existing = lastAgentTimestamp[chatJid];
if (existing) return existing;
const botTs = getLastBotMessageTimestamp(chatJid, ASSISTANT_NAME);
if (botTs) {
logger.info(
{ chatJid, recoveredFrom: botTs },
'Recovered message cursor from last bot reply',
);
lastAgentTimestamp[chatJid] = botTs;
saveState();
return botTs;
}
return '';
}
function saveState(): void {
setRouterState('last_timestamp', lastTimestamp);
setRouterState('last_agent_timestamp', JSON.stringify(lastAgentTimestamp));
@@ -205,11 +228,11 @@ async function processGroupMessages(chatJid: string): Promise<boolean> {
const isMainGroup = group.isMain === true;
const sinceTimestamp = lastAgentTimestamp[chatJid] || '';
const missedMessages = getMessagesSince(
chatJid,
sinceTimestamp,
getOrRecoverCursor(chatJid),
ASSISTANT_NAME,
MAX_MESSAGES_PER_PROMPT,
);
if (missedMessages.length === 0) return true;
@@ -460,8 +483,9 @@ async function startMessageLoop(): Promise<void> {
// context that accumulated between triggers is included.
const allPending = getMessagesSince(
chatJid,
lastAgentTimestamp[chatJid] || '',
getOrRecoverCursor(chatJid),
ASSISTANT_NAME,
MAX_MESSAGES_PER_PROMPT,
);
const messagesToSend =
allPending.length > 0 ? allPending : groupMessages;
@@ -500,8 +524,12 @@ async function startMessageLoop(): Promise<void> {
*/
function recoverPendingMessages(): void {
for (const [chatJid, group] of Object.entries(registeredGroups)) {
const sinceTimestamp = lastAgentTimestamp[chatJid] || '';
const pending = getMessagesSince(chatJid, sinceTimestamp, ASSISTANT_NAME);
const pending = getMessagesSince(
chatJid,
getOrRecoverCursor(chatJid),
ASSISTANT_NAME,
MAX_MESSAGES_PER_PROMPT,
);
if (pending.length > 0) {
logger.info(
{ group: group.name, pendingCount: pending.length },