address review: in-memory history cache, resolveAccount try/catch, include is_from_me

- Wrap resolveAccount in try/catch instead of unreachable guard (it throws)
- Include is_from_me messages with 'me' sender label for full conversation context
- Add in-memory rolling history map (chatHistories) matching other channel patterns
- API backfill only on first message per chat, not every incoming message
- Remove unused buildInboundHistoryFromEntries import
This commit is contained in:
Ryan Mac Mini
2026-02-18 14:36:42 -05:00
parent d988438c0d
commit 1b16f295b3
2 changed files with 69 additions and 29 deletions

View File

@@ -48,8 +48,11 @@ export async function fetchBlueBubblesHistory(
return [];
}
const { baseUrl, password, accountId } = resolveAccount(opts);
if (!baseUrl || !password) {
let baseUrl: string;
let password: string;
try {
({ baseUrl, password } = resolveAccount(opts));
} catch {
return [];
}
@@ -101,13 +104,9 @@ export async function fetchBlueBubblesHistory(
continue;
}
// Skip from-me messages to avoid duplication
if (msg.is_from_me) {
continue;
}
const sender =
msg.sender?.display_name || msg.sender?.address || msg.handle_id || "Unknown";
const sender = msg.is_from_me
? "me"
: msg.sender?.display_name || msg.sender?.address || msg.handle_id || "Unknown";
const timestamp = msg.date_created || msg.date_delivered;
historyEntries.push({

View File

@@ -1,20 +1,19 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk";
import {
buildPendingHistoryContextFromMap,
createReplyPrefixOptions,
logAckFailure,
logInboundDrop,
logTypingFailure,
recordPendingHistoryEntryIfEnabled,
resolveAckReaction,
resolveControlCommandGate,
stripMarkdown,
type HistoryEntry,
} from "openclaw/plugin-sdk";
import { downloadBlueBubblesAttachment } from "./attachments.js";
import { markBlueBubblesChatRead, sendBlueBubblesTyping } from "./chat.js";
import {
fetchBlueBubblesHistory,
buildInboundHistoryFromEntries,
type BlueBubblesHistoryEntry,
} from "./history.js";
import { fetchBlueBubblesHistory } from "./history.js";
import { sendBlueBubblesMedia } from "./media-send.js";
import {
buildMessagePlaceholder,
@@ -242,6 +241,13 @@ function resolveBlueBubblesAckReaction(params: {
}
}
/**
* In-memory rolling history map keyed by chat identifier.
* Populated from incoming messages during the session; API backfill only
* happens on the first message for a given chat (when the map is empty).
*/
const chatHistories = new Map<string, HistoryEntry[]>();
export async function processMessage(
message: NormalizedWebhookMessage,
target: WebhookTarget,
@@ -818,42 +824,77 @@ export async function processMessage(
.trim();
};
// Fetch history for backfill (both groups and DMs)
// History: in-memory rolling map with API backfill on first message per chat
const historyLimit = isGroup
? (account.config.historyLimit ?? 0)
: (account.config.dmHistoryLimit ?? 0);
let inboundHistory: Array<{ sender: string; body: string; timestamp?: number }> | undefined;
const historyKey =
chatGuid ||
chatIdentifier ||
(chatId ? String(chatId) : null) ||
(isGroup ? null : message.senderId) ||
"";
if (historyLimit > 0) {
// Determine chat identifier for history fetching
const historyIdentifier =
chatGuid ||
chatIdentifier ||
(chatId ? String(chatId) : null) ||
(isGroup ? null : message.senderId);
// Record the current message into rolling history
if (historyKey && historyLimit > 0) {
const senderLabel = message.fromMe ? "me" : message.senderName || message.senderId;
recordPendingHistoryEntryIfEnabled({
historyMap: chatHistories,
limit: historyLimit,
historyKey,
entry: text
? {
sender: senderLabel,
body: text,
timestamp: Date.now(),
messageId: message.messageId ?? undefined,
}
: null,
});
if (historyIdentifier) {
// API backfill only on first message (when history map was empty before recording)
const existing = chatHistories.get(historyKey);
if (existing && existing.length <= 1) {
try {
const historyEntries = await fetchBlueBubblesHistory(historyIdentifier, historyLimit, {
const historyEntries = await fetchBlueBubblesHistory(historyKey, historyLimit, {
cfg: config,
accountId: account.accountId,
});
if (historyEntries.length > 0) {
inboundHistory = buildInboundHistoryFromEntries(historyEntries);
// Prepend API history before the current message
const apiEntries: HistoryEntry[] = historyEntries.map((e) => ({
sender: e.sender,
body: e.body,
timestamp: e.timestamp,
messageId: e.messageId,
}));
chatHistories.set(historyKey, [...apiEntries, ...existing]);
logVerbose(
core,
runtime,
`fetched ${historyEntries.length} history messages for ${isGroup ? "group" : "DM"}: ${historyIdentifier}`,
`backfilled ${historyEntries.length} history messages for ${isGroup ? "group" : "DM"}: ${historyKey}`,
);
}
} catch (err) {
logVerbose(core, runtime, `history fetch failed for ${historyIdentifier}: ${String(err)}`);
logVerbose(core, runtime, `history backfill failed for ${historyKey}: ${String(err)}`);
}
}
}
// Build inbound history from the in-memory map
let inboundHistory: Array<{ sender: string; body: string; timestamp?: number }> | undefined;
if (historyKey && historyLimit > 0) {
const entries = chatHistories.get(historyKey);
if (entries && entries.length > 0) {
inboundHistory = entries.map((e) => ({
sender: e.sender,
body: e.body,
timestamp: e.timestamp,
}));
}
}
const ctxPayload = core.channel.reply.finalizeInboundContext({
Body: body,
BodyForAgent: rawBody,