@byte5ai/palaia 2.0.5 → 2.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/hooks.ts +97 -19
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byte5ai/palaia",
3
- "version": "2.0.5",
3
+ "version": "2.0.7",
4
4
  "description": "Palaia memory backend for OpenClaw",
5
5
  "main": "index.ts",
6
6
  "openclaw": {
package/src/hooks.ts CHANGED
@@ -1154,44 +1154,122 @@ export function getLastUserMessage(messages: unknown[]): string | null {
1154
1154
  }
1155
1155
 
1156
1156
  // ============================================================================
1157
- // Recall Query Builder (provenance-based, Issue #65 upgrade)
1157
+ // Channel Envelope Stripping (v2.0.6)
1158
1158
  // ============================================================================
1159
1159
 
1160
1160
  /**
1161
- * Build a recall query from message history using provenance to identify real user input.
1161
+ * Strip OpenClaw channel envelope from message text.
1162
+ * Matches the pattern: [TIMESTAMP] or [CHANNEL TIMESTAMP] prefix
1163
+ * that OpenClaw adds to inbound messages from all channels.
1164
+ * Based on OpenClaw's internal stripEnvelope() logic.
1165
+ */
1166
+ const ENVELOPE_PREFIX_RE = /^\[([^\]]+)\]\s*/;
1167
+ const ENVELOPE_CHANNELS = [
1168
+ "WebChat", "WhatsApp", "Telegram", "Signal", "Slack",
1169
+ "Discord", "Google Chat", "iMessage", "Teams", "Matrix",
1170
+ "Zalo", "Zalo Personal", "BlueBubbles",
1171
+ ];
1172
+
1173
+ function looksLikeEnvelopeHeader(header: string): boolean {
1174
+ // ISO timestamp pattern
1175
+ if (/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}Z\b/.test(header)) return true;
1176
+ // Space-separated timestamp
1177
+ if (/\d{4}-\d{2}-\d{2} \d{2}:\d{2}\b/.test(header)) return true;
1178
+ // Channel prefix
1179
+ return ENVELOPE_CHANNELS.some(ch => header.startsWith(`${ch} `));
1180
+ }
1181
+
1182
+ export function stripChannelEnvelope(text: string): string {
1183
+ const match = text.match(ENVELOPE_PREFIX_RE);
1184
+ if (!match) return text;
1185
+ if (!looksLikeEnvelopeHeader(match[1] ?? "")) return text;
1186
+ return text.slice(match[0].length);
1187
+ }
1188
+
1189
+ /**
1190
+ * Strip "System: [timestamp] Channel message in #channel from User: " prefix.
1191
+ * OpenClaw wraps inbound messages with this pattern for all channel providers.
1192
+ */
1193
+ const SYSTEM_PREFIX_RE = /^System:\s*\[\d{4}-\d{2}-\d{2}[^\]]*\]\s*(?:Slack message|Telegram message|Discord message|WhatsApp message|Signal message|message).*?(?:from \w+:\s*)?/i;
1194
+
1195
+ export function stripSystemPrefix(text: string): string {
1196
+ const match = text.match(SYSTEM_PREFIX_RE);
1197
+ if (match) return text.slice(match[0].length);
1198
+ return text;
1199
+ }
1200
+
1201
+ // ============================================================================
1202
+ // Recall Query Builder (v2.0.6: envelope-aware, provenance-based)
1203
+ // ============================================================================
1204
+
1205
+ /**
1206
+ * Messages that are purely system content (no user text).
1207
+ * Used to skip edited notifications, sync events, inter-session messages, etc.
1208
+ */
1209
+ function isSystemOnlyContent(text: string): boolean {
1210
+ if (!text) return true;
1211
+ if (text.startsWith("System:")) return true;
1212
+ if (text.startsWith("[Queued")) return true;
1213
+ if (text.startsWith("[Inter-session")) return true;
1214
+ if (/^Slack message (edited|deleted)/.test(text)) return true;
1215
+ if (/^\[auto\]/.test(text)) return true;
1216
+ if (text.length < 3) return true;
1217
+ return false;
1218
+ }
1219
+
1220
+ /**
1221
+ * Build a recall query from message history.
1222
+ *
1223
+ * v2.0.6: Strips OpenClaw channel envelopes (System: [...] Slack message from ...:)
1224
+ * and inter-session prefixes before building the query. This prevents envelope
1225
+ * metadata from polluting semantic search and causing timeouts / false-high scores.
1162
1226
  *
1163
- * - Prefers external_user messages (real human input from Slack/Telegram).
1227
+ * - Filters out inter_session and internal_system provenance messages.
1164
1228
  * - Falls back to any user message for backward compat (OpenClaw without provenance).
1165
- * - If the last user message is short (< 30 chars), prepends the previous for context.
1229
+ * - Strips channel envelopes and system prefixes from message text.
1230
+ * - Skips system-only content (edited notifications, sync events).
1231
+ * - Short messages (< 30 chars): prepends previous for context.
1166
1232
  * - Hard-caps at 500 characters.
1167
- *
1168
- * Provenance makes the old heuristic cleaners (DAY_PREFIXES, system marker stripping) obsolete.
1169
1233
  */
1170
1234
  export function buildRecallQuery(messages: unknown[]): string {
1171
1235
  const texts = extractMessageTexts(messages);
1172
1236
 
1173
- // Prefer external_user messages (real human input)
1174
- const externalUserMsgs = texts.filter(
1175
- t => t.role === "user" && t.provenance === "external_user"
1237
+ // Step 1: Filter out inter_session messages (sub-agent results, sessions_send)
1238
+ const candidates = texts.filter(
1239
+ t => t.role === "user" && t.provenance !== "inter_session" && t.provenance !== "internal_system"
1176
1240
  );
1177
1241
 
1178
- // Fallback: any user message (backward compat for OpenClaw without provenance)
1179
- const userMsgs = externalUserMsgs.length > 0
1180
- ? externalUserMsgs
1242
+ // Fallback: if no messages without provenance, use all user messages
1243
+ const userMsgs = candidates.length > 0
1244
+ ? candidates
1181
1245
  : texts.filter(t => t.role === "user");
1182
1246
 
1183
1247
  if (userMsgs.length === 0) return "";
1184
1248
 
1185
- const lastMsg = userMsgs[userMsgs.length - 1].text.trim();
1249
+ // Step 2: Strip envelopes from the last user message(s)
1250
+ let lastText = stripSystemPrefix(stripChannelEnvelope(userMsgs[userMsgs.length - 1].text.trim()));
1186
1251
 
1187
- // Short messages: include previous for context
1188
- if (lastMsg.length < 30 && userMsgs.length > 1) {
1189
- const prevMsg = userMsgs[userMsgs.length - 2].text.trim();
1190
- const combined = `${prevMsg} ${lastMsg}`.slice(0, 500);
1191
- return combined;
1252
+ // Skip system-only messages (edited notifications, sync events, etc.)
1253
+ // Walk backwards to find a message with actual content
1254
+ let idx = userMsgs.length - 1;
1255
+ while (idx >= 0 && (!lastText || isSystemOnlyContent(lastText))) {
1256
+ idx--;
1257
+ if (idx >= 0) {
1258
+ lastText = stripSystemPrefix(stripChannelEnvelope(userMsgs[idx].text.trim()));
1259
+ }
1260
+ }
1261
+
1262
+ if (!lastText) return "";
1263
+
1264
+ // Step 3: Short messages → include previous for context
1265
+ if (lastText.length < 30 && idx > 0) {
1266
+ const prevText = stripSystemPrefix(stripChannelEnvelope(userMsgs[idx - 1].text.trim()));
1267
+ if (prevText && !isSystemOnlyContent(prevText)) {
1268
+ return `${prevText} ${lastText}`.slice(0, 500);
1269
+ }
1192
1270
  }
1193
1271
 
1194
- return lastMsg.slice(0, 500);
1272
+ return lastText.slice(0, 500);
1195
1273
  }
1196
1274
 
1197
1275
  // ============================================================================