@byte5ai/palaia 2.0.4 → 2.0.5
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.
- package/package.json +1 -1
- package/src/hooks.ts +96 -88
package/package.json
CHANGED
package/src/hooks.ts
CHANGED
|
@@ -137,6 +137,16 @@ const lastInboundMessageByChannel = new Map<string, { messageId: string; provide
|
|
|
137
137
|
/** Channels that support emoji reactions. */
|
|
138
138
|
const REACTION_SUPPORTED_PROVIDERS = new Set(["slack", "discord"]);
|
|
139
139
|
|
|
140
|
+
// ============================================================================
|
|
141
|
+
// Logger (Issue: api.logger integration)
|
|
142
|
+
// ============================================================================
|
|
143
|
+
|
|
144
|
+
/** Module-level logger — defaults to console, replaced by api.logger in registerHooks. */
|
|
145
|
+
let logger: { info: (...args: any[]) => void; warn: (...args: any[]) => void } = {
|
|
146
|
+
info: (...args: any[]) => console.log(...args),
|
|
147
|
+
warn: (...args: any[]) => console.warn(...args),
|
|
148
|
+
};
|
|
149
|
+
|
|
140
150
|
// ============================================================================
|
|
141
151
|
// Scope Validation (Issue #90)
|
|
142
152
|
// ============================================================================
|
|
@@ -350,7 +360,7 @@ async function sendSlackReaction(
|
|
|
350
360
|
): Promise<void> {
|
|
351
361
|
const token = await resolveSlackBotToken();
|
|
352
362
|
if (!token) {
|
|
353
|
-
|
|
363
|
+
logger.warn("[palaia] Cannot send Slack reaction: no bot token found");
|
|
354
364
|
return;
|
|
355
365
|
}
|
|
356
366
|
|
|
@@ -375,11 +385,11 @@ async function sendSlackReaction(
|
|
|
375
385
|
});
|
|
376
386
|
const data = await response.json() as { ok: boolean; error?: string };
|
|
377
387
|
if (!data.ok && data.error !== "already_reacted") {
|
|
378
|
-
|
|
388
|
+
logger.warn(`[palaia] Slack reaction failed: ${data.error} (${normalizedEmoji} on ${channelId})`);
|
|
379
389
|
}
|
|
380
390
|
} catch (err) {
|
|
381
391
|
if ((err as Error).name !== "AbortError") {
|
|
382
|
-
|
|
392
|
+
logger.warn(`[palaia] Slack reaction error (${normalizedEmoji}): ${err}`);
|
|
383
393
|
}
|
|
384
394
|
} finally {
|
|
385
395
|
clearTimeout(timeout);
|
|
@@ -792,7 +802,7 @@ export function resolveCaptureModel(
|
|
|
792
802
|
if (parts.length >= 2) {
|
|
793
803
|
if (!_captureModelFallbackWarned) {
|
|
794
804
|
_captureModelFallbackWarned = true;
|
|
795
|
-
|
|
805
|
+
logger.warn(`[palaia] No captureModel configured — using primary model. Set captureModel in plugin config for cost savings.`);
|
|
796
806
|
}
|
|
797
807
|
return { provider: parts[0], model: parts.slice(1).join("/") };
|
|
798
808
|
}
|
|
@@ -825,21 +835,27 @@ function collectText(payloads: Array<{ text?: string; isError?: boolean }> | und
|
|
|
825
835
|
* then hard-cap at maxChars from the end (newest messages kept).
|
|
826
836
|
*/
|
|
827
837
|
export function trimToRecentExchanges(
|
|
828
|
-
texts: Array<{ role: string; text: string }>,
|
|
838
|
+
texts: Array<{ role: string; text: string; provenance?: string }>,
|
|
829
839
|
maxPairs = 5,
|
|
830
840
|
maxChars = 10_000,
|
|
831
|
-
): Array<{ role: string; text: string }> {
|
|
841
|
+
): Array<{ role: string; text: string; provenance?: string }> {
|
|
832
842
|
// Filter to only user + assistant messages (skip tool, toolResult, system, etc.)
|
|
833
843
|
const exchanges = texts.filter((t) => t.role === "user" || t.role === "assistant");
|
|
834
844
|
|
|
835
845
|
// Keep the last N pairs (a pair = one user + one assistant message)
|
|
846
|
+
// Only count external_user messages as real user turns.
|
|
847
|
+
// System-injected user messages (inter_session, internal_system) don't count as conversation turns.
|
|
836
848
|
// Walk backwards, count pairs
|
|
837
849
|
let pairCount = 0;
|
|
838
850
|
let lastRole = "";
|
|
839
851
|
let cutIndex = 0; // default: keep everything
|
|
840
852
|
for (let i = exchanges.length - 1; i >= 0; i--) {
|
|
841
|
-
|
|
842
|
-
|
|
853
|
+
const isRealUser = exchanges[i].role === "user" && (
|
|
854
|
+
exchanges[i].provenance === "external_user" ||
|
|
855
|
+
!exchanges[i].provenance // backward compat: no provenance = treat as real user
|
|
856
|
+
);
|
|
857
|
+
// Count a new pair when we see a real user message after having seen an assistant
|
|
858
|
+
if (isRealUser && lastRole === "assistant") {
|
|
843
859
|
pairCount++;
|
|
844
860
|
if (pairCount > maxPairs) {
|
|
845
861
|
cutIndex = i + 1; // keep from next message onwards
|
|
@@ -1087,8 +1103,8 @@ export function extractSignificance(
|
|
|
1087
1103
|
return { tags, type: primaryType, summary };
|
|
1088
1104
|
}
|
|
1089
1105
|
|
|
1090
|
-
export function extractMessageTexts(messages: unknown[]): Array<{ role: string; text: string }> {
|
|
1091
|
-
const result: Array<{ role: string; text: string }> = [];
|
|
1106
|
+
export function extractMessageTexts(messages: unknown[]): Array<{ role: string; text: string; provenance?: string }> {
|
|
1107
|
+
const result: Array<{ role: string; text: string; provenance?: string }> = [];
|
|
1092
1108
|
|
|
1093
1109
|
for (const msg of messages) {
|
|
1094
1110
|
if (!msg || typeof msg !== "object") continue;
|
|
@@ -1096,8 +1112,12 @@ export function extractMessageTexts(messages: unknown[]): Array<{ role: string;
|
|
|
1096
1112
|
const role = m.role;
|
|
1097
1113
|
if (!role || typeof role !== "string") continue;
|
|
1098
1114
|
|
|
1115
|
+
// Extract provenance kind (string or object with .kind)
|
|
1116
|
+
const rawProvenance = (m as any).provenance?.kind ?? (m as any).provenance;
|
|
1117
|
+
const provenance = typeof rawProvenance === "string" ? rawProvenance : undefined;
|
|
1118
|
+
|
|
1099
1119
|
if (typeof m.content === "string" && m.content.trim()) {
|
|
1100
|
-
result.push({ role, text: m.content.trim() });
|
|
1120
|
+
result.push({ role, text: m.content.trim(), provenance });
|
|
1101
1121
|
continue;
|
|
1102
1122
|
}
|
|
1103
1123
|
|
|
@@ -1110,7 +1130,7 @@ export function extractMessageTexts(messages: unknown[]): Array<{ role: string;
|
|
|
1110
1130
|
typeof block.text === "string" &&
|
|
1111
1131
|
block.text.trim()
|
|
1112
1132
|
) {
|
|
1113
|
-
result.push({ role, text: block.text.trim() });
|
|
1133
|
+
result.push({ role, text: block.text.trim(), provenance });
|
|
1114
1134
|
}
|
|
1115
1135
|
}
|
|
1116
1136
|
}
|
|
@@ -1121,6 +1141,12 @@ export function extractMessageTexts(messages: unknown[]): Array<{ role: string;
|
|
|
1121
1141
|
|
|
1122
1142
|
export function getLastUserMessage(messages: unknown[]): string | null {
|
|
1123
1143
|
const texts = extractMessageTexts(messages);
|
|
1144
|
+
// Prefer external_user provenance (real human input)
|
|
1145
|
+
for (let i = texts.length - 1; i >= 0; i--) {
|
|
1146
|
+
if (texts[i].role === "user" && texts[i].provenance === "external_user")
|
|
1147
|
+
return texts[i].text;
|
|
1148
|
+
}
|
|
1149
|
+
// Fallback: any user message (backward compat for OpenClaw without provenance)
|
|
1124
1150
|
for (let i = texts.length - 1; i >= 0; i--) {
|
|
1125
1151
|
if (texts[i].role === "user") return texts[i].text;
|
|
1126
1152
|
}
|
|
@@ -1128,80 +1154,44 @@ export function getLastUserMessage(messages: unknown[]): string | null {
|
|
|
1128
1154
|
}
|
|
1129
1155
|
|
|
1130
1156
|
// ============================================================================
|
|
1131
|
-
// Recall Query Builder (Issue #65 upgrade
|
|
1157
|
+
// Recall Query Builder (provenance-based, Issue #65 upgrade)
|
|
1132
1158
|
// ============================================================================
|
|
1133
1159
|
|
|
1134
|
-
/** Day-of-week prefixes used as system markers in messages. */
|
|
1135
|
-
const DAY_PREFIXES = ["[Mon ", "[Tue ", "[Wed ", "[Thu ", "[Fri ", "[Sat ", "[Sun "];
|
|
1136
|
-
|
|
1137
1160
|
/**
|
|
1138
|
-
*
|
|
1139
|
-
* and other noise that degrades semantic search quality.
|
|
1140
|
-
*/
|
|
1141
|
-
export function cleanMessageForQuery(text: string): string {
|
|
1142
|
-
let cleaned = text;
|
|
1143
|
-
|
|
1144
|
-
// Remove JSON code blocks (```json ... ```)
|
|
1145
|
-
cleaned = cleaned.replace(/```json[\s\S]*?```/gi, "");
|
|
1146
|
-
|
|
1147
|
-
// Remove lines starting with system markers
|
|
1148
|
-
cleaned = cleaned
|
|
1149
|
-
.split("\n")
|
|
1150
|
-
.filter((line) => {
|
|
1151
|
-
const trimmed = line.trimStart();
|
|
1152
|
-
if (trimmed.startsWith("System:")) return false;
|
|
1153
|
-
if (trimmed.startsWith("[Queued")) return false;
|
|
1154
|
-
if (trimmed.startsWith("[Inter-session")) return false;
|
|
1155
|
-
for (const prefix of DAY_PREFIXES) {
|
|
1156
|
-
if (trimmed.startsWith(prefix)) return false;
|
|
1157
|
-
}
|
|
1158
|
-
return true;
|
|
1159
|
-
})
|
|
1160
|
-
.join("\n")
|
|
1161
|
-
.trim();
|
|
1162
|
-
|
|
1163
|
-
return cleaned;
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
/**
|
|
1167
|
-
* Build a recall query from message history.
|
|
1161
|
+
* Build a recall query from message history using provenance to identify real user input.
|
|
1168
1162
|
*
|
|
1169
|
-
* -
|
|
1170
|
-
* -
|
|
1171
|
-
*
|
|
1172
|
-
* - Strips system markers, JSON blocks, and other noise.
|
|
1163
|
+
* - Prefers external_user messages (real human input from Slack/Telegram).
|
|
1164
|
+
* - 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.
|
|
1173
1166
|
* - Hard-caps at 500 characters.
|
|
1174
1167
|
*
|
|
1175
|
-
*
|
|
1168
|
+
* Provenance makes the old heuristic cleaners (DAY_PREFIXES, system marker stripping) obsolete.
|
|
1176
1169
|
*/
|
|
1177
1170
|
export function buildRecallQuery(messages: unknown[]): string {
|
|
1178
1171
|
const texts = extractMessageTexts(messages);
|
|
1179
|
-
const userMessages: string[] = [];
|
|
1180
|
-
for (let i = texts.length - 1; i >= 0 && userMessages.length < 2; i--) {
|
|
1181
|
-
if (texts[i].role === "user") {
|
|
1182
|
-
const cleaned = cleanMessageForQuery(texts[i].text);
|
|
1183
|
-
if (cleaned) userMessages.unshift(cleaned);
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
1172
|
|
|
1187
|
-
|
|
1173
|
+
// Prefer external_user messages (real human input)
|
|
1174
|
+
const externalUserMsgs = texts.filter(
|
|
1175
|
+
t => t.role === "user" && t.provenance === "external_user"
|
|
1176
|
+
);
|
|
1177
|
+
|
|
1178
|
+
// Fallback: any user message (backward compat for OpenClaw without provenance)
|
|
1179
|
+
const userMsgs = externalUserMsgs.length > 0
|
|
1180
|
+
? externalUserMsgs
|
|
1181
|
+
: texts.filter(t => t.role === "user");
|
|
1188
1182
|
|
|
1189
|
-
|
|
1183
|
+
if (userMsgs.length === 0) return "";
|
|
1190
1184
|
|
|
1191
|
-
|
|
1192
|
-
let query: string;
|
|
1193
|
-
if (lastMsg.length < 30 && userMessages.length > 1) {
|
|
1194
|
-
query = `${userMessages[userMessages.length - 2]} ${lastMsg}`;
|
|
1195
|
-
} else {
|
|
1196
|
-
query = lastMsg;
|
|
1197
|
-
}
|
|
1185
|
+
const lastMsg = userMsgs[userMsgs.length - 1].text.trim();
|
|
1198
1186
|
|
|
1199
|
-
//
|
|
1200
|
-
if (
|
|
1201
|
-
|
|
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;
|
|
1202
1192
|
}
|
|
1203
1193
|
|
|
1204
|
-
return
|
|
1194
|
+
return lastMsg.slice(0, 500);
|
|
1205
1195
|
}
|
|
1206
1196
|
|
|
1207
1197
|
// ============================================================================
|
|
@@ -1304,22 +1294,27 @@ export function resetTurnState(): void {
|
|
|
1304
1294
|
* Register lifecycle hooks on the plugin API.
|
|
1305
1295
|
*/
|
|
1306
1296
|
export function registerHooks(api: any, config: PalaiaPluginConfig): void {
|
|
1297
|
+
// Store api.logger for module-wide use (integrates into OpenClaw log system)
|
|
1298
|
+
if (api.logger && typeof api.logger.info === "function") {
|
|
1299
|
+
logger = api.logger;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1307
1302
|
const opts = buildRunnerOpts(config);
|
|
1308
1303
|
|
|
1309
|
-
// ── Startup checks (H-2, H-3)
|
|
1304
|
+
// ── Startup checks (H-2, H-3, captureModel validation) ────────
|
|
1310
1305
|
(async () => {
|
|
1311
1306
|
// H-2: Warn if no agent is configured
|
|
1312
1307
|
if (!process.env.PALAIA_AGENT) {
|
|
1313
1308
|
try {
|
|
1314
1309
|
const statusOut = await run(["config", "get", "agent"], { ...opts, timeoutMs: 3000 });
|
|
1315
1310
|
if (!statusOut.trim()) {
|
|
1316
|
-
|
|
1311
|
+
logger.warn(
|
|
1317
1312
|
"[palaia] No agent configured. Set PALAIA_AGENT env var or run 'palaia init --agent <name>'. " +
|
|
1318
1313
|
"Auto-captured entries will have no agent attribution."
|
|
1319
1314
|
);
|
|
1320
1315
|
}
|
|
1321
1316
|
} catch {
|
|
1322
|
-
|
|
1317
|
+
logger.warn(
|
|
1323
1318
|
"[palaia] No agent configured. Set PALAIA_AGENT env var or run 'palaia init --agent <name>'. " +
|
|
1324
1319
|
"Auto-captured entries will have no agent attribution."
|
|
1325
1320
|
);
|
|
@@ -1346,7 +1341,7 @@ export function registerHooks(api: any, config: PalaiaPluginConfig): void {
|
|
|
1346
1341
|
|| status.config?.embedding_provider
|
|
1347
1342
|
);
|
|
1348
1343
|
if (!hasSemanticProvider && !hasProviderConfig) {
|
|
1349
|
-
|
|
1344
|
+
logger.warn(
|
|
1350
1345
|
"[palaia] No embedding provider configured. Semantic search is inactive (BM25 keyword-only). " +
|
|
1351
1346
|
"Run 'pip install palaia[fastembed]' and 'palaia doctor --fix' for better recall quality."
|
|
1352
1347
|
);
|
|
@@ -1356,6 +1351,19 @@ export function registerHooks(api: any, config: PalaiaPluginConfig): void {
|
|
|
1356
1351
|
} catch {
|
|
1357
1352
|
// Non-fatal — status check failed, skip warning (avoid false positive)
|
|
1358
1353
|
}
|
|
1354
|
+
|
|
1355
|
+
// Validate captureModel auth at plugin startup via modelAuth API
|
|
1356
|
+
if (config.captureModel && api.runtime?.modelAuth) {
|
|
1357
|
+
try {
|
|
1358
|
+
const resolved = resolveCaptureModel(api.config, config.captureModel);
|
|
1359
|
+
if (resolved?.provider) {
|
|
1360
|
+
const key = await api.runtime.modelAuth.resolveApiKeyForProvider({ provider: resolved.provider, cfg: api.config });
|
|
1361
|
+
if (!key) {
|
|
1362
|
+
logger.warn(`[palaia] captureModel provider "${resolved.provider}" has no API key — auto-capture LLM extraction will fail`);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
} catch { /* non-fatal */ }
|
|
1366
|
+
}
|
|
1359
1367
|
})();
|
|
1360
1368
|
|
|
1361
1369
|
// ── /palaia status command ─────────────────────────────────────
|
|
@@ -1445,7 +1453,7 @@ export function registerHooks(api: any, config: PalaiaPluginConfig): void {
|
|
|
1445
1453
|
entries = result.results;
|
|
1446
1454
|
}
|
|
1447
1455
|
} catch (queryError) {
|
|
1448
|
-
|
|
1456
|
+
logger.warn(`[palaia] Query recall failed, falling back to list: ${queryError}`);
|
|
1449
1457
|
}
|
|
1450
1458
|
}
|
|
1451
1459
|
}
|
|
@@ -1556,7 +1564,7 @@ export function registerHooks(api: any, config: PalaiaPluginConfig): void {
|
|
|
1556
1564
|
: undefined,
|
|
1557
1565
|
};
|
|
1558
1566
|
} catch (error) {
|
|
1559
|
-
|
|
1567
|
+
logger.warn(`[palaia] Memory injection failed: ${error}`);
|
|
1560
1568
|
}
|
|
1561
1569
|
});
|
|
1562
1570
|
}
|
|
@@ -1669,7 +1677,7 @@ export function registerHooks(api: any, config: PalaiaPluginConfig): void {
|
|
|
1669
1677
|
(p) => p.name.toLowerCase() === validatedProject!.toLowerCase(),
|
|
1670
1678
|
);
|
|
1671
1679
|
if (!isKnown) {
|
|
1672
|
-
|
|
1680
|
+
logger.info(`[palaia] Auto-capture: unknown project "${validatedProject}" ignored`);
|
|
1673
1681
|
validatedProject = null;
|
|
1674
1682
|
}
|
|
1675
1683
|
}
|
|
@@ -1686,7 +1694,7 @@ export function registerHooks(api: any, config: PalaiaPluginConfig): void {
|
|
|
1686
1694
|
effectiveScope,
|
|
1687
1695
|
);
|
|
1688
1696
|
await run(args, { ...opts, timeoutMs: 10_000 });
|
|
1689
|
-
|
|
1697
|
+
logger.info(
|
|
1690
1698
|
`[palaia] LLM auto-captured: type=${r.type}, significance=${r.significance}, tags=${tags.join(",")}, project=${validatedProject || "none"}, scope=${effectiveScope || "team"}`
|
|
1691
1699
|
);
|
|
1692
1700
|
}
|
|
@@ -1711,7 +1719,7 @@ export function registerHooks(api: any, config: PalaiaPluginConfig): void {
|
|
|
1711
1719
|
// captureModel is broken — try primary model as fallback
|
|
1712
1720
|
if (!_captureModelFailoverWarned) {
|
|
1713
1721
|
_captureModelFailoverWarned = true;
|
|
1714
|
-
|
|
1722
|
+
logger.warn(`[palaia] WARNING: captureModel failed (${errStr}). Using primary model as fallback. Please update captureModel in your config.`);
|
|
1715
1723
|
}
|
|
1716
1724
|
try {
|
|
1717
1725
|
// Retry without captureModel → resolveCaptureModel will use primary model
|
|
@@ -1722,13 +1730,13 @@ export function registerHooks(api: any, config: PalaiaPluginConfig): void {
|
|
|
1722
1730
|
llmHandled = true;
|
|
1723
1731
|
} catch (fallbackError) {
|
|
1724
1732
|
if (!_llmImportFailureLogged) {
|
|
1725
|
-
|
|
1733
|
+
logger.warn(`[palaia] LLM extraction failed (primary model fallback also failed): ${fallbackError}`);
|
|
1726
1734
|
_llmImportFailureLogged = true;
|
|
1727
1735
|
}
|
|
1728
1736
|
}
|
|
1729
1737
|
} else {
|
|
1730
1738
|
if (!_llmImportFailureLogged) {
|
|
1731
|
-
|
|
1739
|
+
logger.warn(`[palaia] LLM extraction failed, using rule-based fallback: ${llmError}`);
|
|
1732
1740
|
_llmImportFailureLogged = true;
|
|
1733
1741
|
}
|
|
1734
1742
|
}
|
|
@@ -1770,7 +1778,7 @@ export function registerHooks(api: any, config: PalaiaPluginConfig): void {
|
|
|
1770
1778
|
);
|
|
1771
1779
|
|
|
1772
1780
|
await run(args, { ...opts, timeoutMs: 10_000 });
|
|
1773
|
-
|
|
1781
|
+
logger.info(
|
|
1774
1782
|
`[palaia] Rule-based auto-captured: type=${captureData.type}, tags=${captureData.tags.join(",")}`
|
|
1775
1783
|
);
|
|
1776
1784
|
}
|
|
@@ -1782,7 +1790,7 @@ export function registerHooks(api: any, config: PalaiaPluginConfig): void {
|
|
|
1782
1790
|
} else {
|
|
1783
1791
|
}
|
|
1784
1792
|
} catch (error) {
|
|
1785
|
-
|
|
1793
|
+
logger.warn(`[palaia] Auto-capture failed: ${error}`);
|
|
1786
1794
|
}
|
|
1787
1795
|
|
|
1788
1796
|
// ── Emoji Reactions (Issue #87) ──────────────────────────
|
|
@@ -1814,7 +1822,7 @@ export function registerHooks(api: any, config: PalaiaPluginConfig): void {
|
|
|
1814
1822
|
}
|
|
1815
1823
|
}
|
|
1816
1824
|
} catch (reactionError) {
|
|
1817
|
-
|
|
1825
|
+
logger.warn(`[palaia] Reaction sending failed: ${reactionError}`);
|
|
1818
1826
|
} finally {
|
|
1819
1827
|
// Always clean up turn state
|
|
1820
1828
|
deleteTurnState(sessionKey);
|
|
@@ -1844,7 +1852,7 @@ export function registerHooks(api: any, config: PalaiaPluginConfig): void {
|
|
|
1844
1852
|
}
|
|
1845
1853
|
}
|
|
1846
1854
|
} catch (err) {
|
|
1847
|
-
|
|
1855
|
+
logger.warn(`[palaia] Recall reaction failed: ${err}`);
|
|
1848
1856
|
} finally {
|
|
1849
1857
|
deleteTurnState(sessionKey);
|
|
1850
1858
|
}
|
|
@@ -1857,10 +1865,10 @@ export function registerHooks(api: any, config: PalaiaPluginConfig): void {
|
|
|
1857
1865
|
start: async () => {
|
|
1858
1866
|
const result = await recover(opts);
|
|
1859
1867
|
if (result.replayed > 0) {
|
|
1860
|
-
|
|
1868
|
+
logger.info(`[palaia] WAL recovery: replayed ${result.replayed} entries`);
|
|
1861
1869
|
}
|
|
1862
1870
|
if (result.errors > 0) {
|
|
1863
|
-
|
|
1871
|
+
logger.warn(`[palaia] WAL recovery completed with ${result.errors} error(s)`);
|
|
1864
1872
|
}
|
|
1865
1873
|
},
|
|
1866
1874
|
});
|