@byte5ai/palaia 2.3.6 → 2.5.1
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/README.md +1 -1
- package/index.ts +42 -17
- package/package.json +2 -2
- package/skill/SKILL.md +358 -8
- package/src/config.ts +17 -0
- package/src/context-engine.ts +440 -297
- package/src/hooks/capture.ts +22 -5
- package/src/hooks/index.ts +101 -33
- package/src/hooks/recall.ts +65 -2
- package/src/hooks/session.ts +464 -0
- package/src/hooks/state.ts +102 -0
- package/src/priorities.ts +12 -0
- package/src/tools.ts +31 -7
- package/src/types.ts +409 -22
package/src/hooks/capture.ts
CHANGED
|
@@ -144,7 +144,10 @@ function resolveExtensionAPIPath(): string | null {
|
|
|
144
144
|
|
|
145
145
|
// Strategy 3: Sibling in global node_modules (plugin installed alongside openclaw)
|
|
146
146
|
try {
|
|
147
|
-
|
|
147
|
+
// __dirname is always available in CJS (our tsconfig module); the ESM
|
|
148
|
+
// fallback via import.meta.url is kept for jiti/ESM loaders at runtime.
|
|
149
|
+
// @ts-expect-error import.meta is valid at runtime under jiti/ESM but TS module=commonjs rejects it
|
|
150
|
+
const thisFile: string = typeof __dirname !== "undefined" ? __dirname : path.dirname(new URL(import.meta.url).pathname);
|
|
148
151
|
// Walk up from plugin src/dist to node_modules, then into openclaw
|
|
149
152
|
let dir = thisFile;
|
|
150
153
|
for (let i = 0; i < 6; i++) {
|
|
@@ -469,11 +472,11 @@ export async function extractWithLLM(
|
|
|
469
472
|
}
|
|
470
473
|
|
|
471
474
|
const allTexts = extractMessageTexts(messages);
|
|
472
|
-
// Strip Palaia-injected recall context from user messages
|
|
475
|
+
// Strip Palaia-injected recall context and private blocks from user messages
|
|
473
476
|
const cleanedTexts = allTexts.map(t =>
|
|
474
477
|
t.role === "user"
|
|
475
|
-
? { ...t, text: stripPalaiaInjectedContext(t.text) }
|
|
476
|
-
: t
|
|
478
|
+
? { ...t, text: stripPrivateBlocks(stripPalaiaInjectedContext(t.text)) }
|
|
479
|
+
: { ...t, text: stripPrivateBlocks(t.text) }
|
|
477
480
|
);
|
|
478
481
|
// Only extract from recent exchanges — full history causes LLM timeouts
|
|
479
482
|
// and dilutes extraction quality
|
|
@@ -736,5 +739,19 @@ export function stripPalaiaInjectedContext(text: string): string {
|
|
|
736
739
|
// Pattern: "## Active Memory (Palaia)" ... "[palaia] auto-capture=on..." + optional trailing newlines
|
|
737
740
|
// The nudge line is always present and marks the end of the injected block
|
|
738
741
|
const PALAIA_BLOCK_RE = /## Active Memory \(Palaia\)[\s\S]*?\[palaia\][^\n]*\n*/;
|
|
739
|
-
|
|
742
|
+
// Also strip Session Briefing blocks
|
|
743
|
+
const BRIEFING_BLOCK_RE = /## Session Briefing \(Palaia\)[\s\S]*?(?=\n##|\n\n\n|$)/;
|
|
744
|
+
return text
|
|
745
|
+
.replace(PALAIA_BLOCK_RE, '')
|
|
746
|
+
.replace(BRIEFING_BLOCK_RE, '')
|
|
747
|
+
.trim();
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Strip <private>...</private> blocks from text.
|
|
752
|
+
* Content inside private tags is excluded from memory capture.
|
|
753
|
+
* Inspired by claude-mem's privacy marker system.
|
|
754
|
+
*/
|
|
755
|
+
export function stripPrivateBlocks(text: string): string {
|
|
756
|
+
return text.replace(/<private>[\s\S]*?<\/private>/gi, '').trim();
|
|
740
757
|
}
|
package/src/hooks/index.ts
CHANGED
|
@@ -119,6 +119,8 @@ import {
|
|
|
119
119
|
extractMessageTexts,
|
|
120
120
|
buildRecallQuery,
|
|
121
121
|
rerankByTypeWeight,
|
|
122
|
+
formatEntryLine,
|
|
123
|
+
shouldUseCompactMode,
|
|
122
124
|
} from "./recall.js";
|
|
123
125
|
|
|
124
126
|
import {
|
|
@@ -151,6 +153,9 @@ import {
|
|
|
151
153
|
filterBlocked,
|
|
152
154
|
} from "../priorities.js";
|
|
153
155
|
|
|
156
|
+
import { formatBriefing } from "./session.js";
|
|
157
|
+
import { getOrCreateSessionState } from "./state.js";
|
|
158
|
+
|
|
154
159
|
// ============================================================================
|
|
155
160
|
// Logger (Issue: api.logger integration)
|
|
156
161
|
// ============================================================================
|
|
@@ -190,6 +195,10 @@ export function registerHooks(api: OpenClawPluginApi, config: PalaiaPluginConfig
|
|
|
190
195
|
|
|
191
196
|
const opts = buildRunnerOpts(config);
|
|
192
197
|
|
|
198
|
+
// Note: Session lifecycle hooks (session_start, session_end, before_reset,
|
|
199
|
+
// llm_input, llm_output, after_tool_call) are registered in index.ts entry
|
|
200
|
+
// point BEFORE this function, so they work for both ContextEngine and legacy paths.
|
|
201
|
+
|
|
193
202
|
// ── Startup checks (H-2, H-3, captureModel validation) ────────
|
|
194
203
|
(async () => {
|
|
195
204
|
// H-2: Warn if no agent is configured
|
|
@@ -326,7 +335,7 @@ export function registerHooks(api: OpenClawPluginApi, config: PalaiaPluginConfig
|
|
|
326
335
|
}
|
|
327
336
|
});
|
|
328
337
|
|
|
329
|
-
// ── before_prompt_build (Issue #65: Query-based Recall)
|
|
338
|
+
// ── before_prompt_build (Issue #65: Query-based Recall + v3.0 Session Briefing) ──
|
|
330
339
|
if (config.memoryInject) {
|
|
331
340
|
api.on("before_prompt_build", async (event: any, ctx: any) => {
|
|
332
341
|
// Prune stale entries to prevent memory leaks from crashed sessions (C-2)
|
|
@@ -337,6 +346,34 @@ export function registerHooks(api: OpenClawPluginApi, config: PalaiaPluginConfig
|
|
|
337
346
|
const hookOpts = buildRunnerOpts(config, { workspace: resolved.workspace });
|
|
338
347
|
|
|
339
348
|
try {
|
|
349
|
+
// ── Session Briefing Injection (v3.0) ─────────────────────
|
|
350
|
+
// If a session briefing is pending (from session_start or model switch),
|
|
351
|
+
// prepend it to the recall context for seamless session continuity.
|
|
352
|
+
let briefingText = "";
|
|
353
|
+
let briefingSummary: string | null = null; // Kept for smart query fallback
|
|
354
|
+
const sessionKey = resolveSessionKeyFromCtx(ctx);
|
|
355
|
+
if (sessionKey) {
|
|
356
|
+
const sessState = getOrCreateSessionState(sessionKey);
|
|
357
|
+
// Wait for session_start briefing load (max 3s to avoid blocking)
|
|
358
|
+
if (sessState.briefingReady) {
|
|
359
|
+
await Promise.race([
|
|
360
|
+
sessState.briefingReady,
|
|
361
|
+
new Promise<void>(r => setTimeout(r, 3000)),
|
|
362
|
+
]);
|
|
363
|
+
}
|
|
364
|
+
// Capture summary BEFORE clearing, for smart query fallback below
|
|
365
|
+
briefingSummary = sessState.pendingBriefing?.summary ?? null;
|
|
366
|
+
if (sessState.pendingBriefing && !sessState.briefingDelivered) {
|
|
367
|
+
briefingText = formatBriefing(sessState.pendingBriefing, config.sessionBriefingMaxChars);
|
|
368
|
+
sessState.briefingDelivered = true;
|
|
369
|
+
// Clear pending briefing after delivery (unless model switch re-triggers)
|
|
370
|
+
if (!sessState.modelSwitchDetected) {
|
|
371
|
+
sessState.pendingBriefing = null;
|
|
372
|
+
}
|
|
373
|
+
sessState.modelSwitchDetected = false;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
340
377
|
// Load and resolve priorities (Issue #121)
|
|
341
378
|
const prio = await loadPriorities(resolved.workspace);
|
|
342
379
|
const project = config.captureProject || undefined;
|
|
@@ -347,15 +384,25 @@ export function registerHooks(api: OpenClawPluginApi, config: PalaiaPluginConfig
|
|
|
347
384
|
tier: config.tier,
|
|
348
385
|
}, resolved.agentId, project);
|
|
349
386
|
|
|
350
|
-
|
|
387
|
+
// Reduce recall budget by briefing size
|
|
388
|
+
const maxChars = Math.max((resolvedPrio.maxInjectedChars || 4000) - briefingText.length, 500);
|
|
351
389
|
const limit = Math.min(config.maxResults || 10, 20);
|
|
352
390
|
let entries: QueryResult["results"] = [];
|
|
353
391
|
|
|
354
392
|
if (config.recallMode === "query") {
|
|
355
|
-
|
|
393
|
+
let userMessage = event.messages
|
|
356
394
|
? buildRecallQuery(event.messages)
|
|
357
395
|
: (event.prompt || null);
|
|
358
396
|
|
|
397
|
+
// ── Smart Query Fallback (v3.0) ───────────────────────
|
|
398
|
+
// If query is too short or matches a continuation pattern,
|
|
399
|
+
// use the session summary as query for better recall results.
|
|
400
|
+
const CONTINUATION_PATTERN = /^(ja|ok|weiter|mach|genau|do it|yes|continue|go|proceed|sure|klar|passt|yep|yup|exactly|right)\b/i;
|
|
401
|
+
if (briefingSummary && userMessage && (userMessage.length < 10 || CONTINUATION_PATTERN.test(userMessage.trim()))) {
|
|
402
|
+
userMessage = briefingSummary.slice(0, 500);
|
|
403
|
+
logger.info("[palaia] Smart query fallback: using session summary as recall query");
|
|
404
|
+
}
|
|
405
|
+
|
|
359
406
|
if (userMessage && userMessage.length >= 5) {
|
|
360
407
|
// Try embed server first (fast path: ~0.5s), then CLI fallback (~3-14s)
|
|
361
408
|
let serverQueried = false;
|
|
@@ -372,6 +419,7 @@ export function registerHooks(api: OpenClawPluginApi, config: PalaiaPluginConfig
|
|
|
372
419
|
text: userMessage,
|
|
373
420
|
top_k: limit,
|
|
374
421
|
include_cold: resolvedPrio.tier === "all",
|
|
422
|
+
...(resolvedPrio.scopeVisibility ? { scope_visibility: resolvedPrio.scopeVisibility } : {}),
|
|
375
423
|
}, config.timeoutMs || 3000);
|
|
376
424
|
if (resp?.result?.results && Array.isArray(resp.result.results)) {
|
|
377
425
|
entries = resp.result.results;
|
|
@@ -417,36 +465,37 @@ export function registerHooks(api: OpenClawPluginApi, config: PalaiaPluginConfig
|
|
|
417
465
|
entries = result.results;
|
|
418
466
|
}
|
|
419
467
|
} catch {
|
|
468
|
+
// Still deliver briefing even if recall fails completely
|
|
469
|
+
if (briefingText) {
|
|
470
|
+
return { prependContext: briefingText };
|
|
471
|
+
}
|
|
420
472
|
return;
|
|
421
473
|
}
|
|
422
474
|
}
|
|
423
475
|
|
|
424
|
-
|
|
476
|
+
// If no recall entries but briefing exists, deliver briefing alone
|
|
477
|
+
if (entries.length === 0) {
|
|
478
|
+
if (briefingText) {
|
|
479
|
+
return { prependContext: briefingText };
|
|
480
|
+
}
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
425
483
|
|
|
426
484
|
// Apply type-weighted reranking and blocked filtering (Issue #121)
|
|
427
|
-
const rankedRaw = rerankByTypeWeight(entries, resolvedPrio.recallTypeWeight);
|
|
485
|
+
const rankedRaw = rerankByTypeWeight(entries, resolvedPrio.recallTypeWeight, config.recallRecencyBoost);
|
|
428
486
|
const ranked = filterBlocked(rankedRaw, resolvedPrio.blocked);
|
|
429
487
|
|
|
430
|
-
// Build context string with char budget
|
|
431
|
-
|
|
432
|
-
const
|
|
433
|
-
|
|
488
|
+
// Build context string with char budget
|
|
489
|
+
// Progressive disclosure: compact mode for large stores (title + first line + ID)
|
|
490
|
+
const compact = shouldUseCompactMode(ranked.length);
|
|
434
491
|
let text = "## Active Memory (Palaia)\n\n";
|
|
492
|
+
if (compact) {
|
|
493
|
+
text += "_Compact mode — use `memory_get <id>` for full details._\n\n";
|
|
494
|
+
}
|
|
435
495
|
let chars = text.length;
|
|
436
496
|
|
|
437
497
|
for (const entry of ranked) {
|
|
438
|
-
const
|
|
439
|
-
const typeKey = TYPE_SHORT[entry.type] || entry.type;
|
|
440
|
-
const prefix = `[${scopeKey}/${typeKey}]`;
|
|
441
|
-
|
|
442
|
-
// If body starts with title (common), skip title to save tokens
|
|
443
|
-
let line: string;
|
|
444
|
-
if (entry.body.toLowerCase().startsWith(entry.title.toLowerCase())) {
|
|
445
|
-
line = `${prefix} ${entry.body}\n\n`;
|
|
446
|
-
} else {
|
|
447
|
-
line = `${prefix} ${entry.title}\n${entry.body}\n\n`;
|
|
448
|
-
}
|
|
449
|
-
|
|
498
|
+
const line = formatEntryLine(entry, compact);
|
|
450
499
|
if (chars + line.length > maxChars) break;
|
|
451
500
|
text += line;
|
|
452
501
|
chars += line.length;
|
|
@@ -483,7 +532,6 @@ export function registerHooks(api: OpenClawPluginApi, config: PalaiaPluginConfig
|
|
|
483
532
|
const hasRelevantRecall = !isListFallback && entries.some(
|
|
484
533
|
(e) => typeof e.score === "number" && e.score >= resolvedPrio.recallMinScore,
|
|
485
534
|
);
|
|
486
|
-
const sessionKey = resolveSessionKeyFromCtx(ctx);
|
|
487
535
|
if (sessionKey && hasRelevantRecall) {
|
|
488
536
|
const turnState = getOrCreateTurnState(sessionKey);
|
|
489
537
|
turnState.recallOccurred = true;
|
|
@@ -506,7 +554,7 @@ export function registerHooks(api: OpenClawPluginApi, config: PalaiaPluginConfig
|
|
|
506
554
|
|
|
507
555
|
// Return prependContext + appendSystemContext for recall emoji
|
|
508
556
|
return {
|
|
509
|
-
prependContext:
|
|
557
|
+
prependContext: briefingText + text,
|
|
510
558
|
appendSystemContext: config.showMemorySources
|
|
511
559
|
? "You used Palaia memory in this turn. Add \u{1f9e0} at the very end of your response (after everything else, on its own line)."
|
|
512
560
|
: undefined,
|
|
@@ -559,14 +607,17 @@ export function registerHooks(api: OpenClawPluginApi, config: PalaiaPluginConfig
|
|
|
559
607
|
collectedHints.push(...hints);
|
|
560
608
|
}
|
|
561
609
|
|
|
562
|
-
// Strip Palaia-injected recall context
|
|
610
|
+
// Strip Palaia-injected recall context and private blocks from messages.
|
|
563
611
|
// The recall block is prepended to user messages by before_prompt_build.
|
|
564
612
|
// Without stripping, auto-capture would re-capture previously recalled memories.
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
613
|
+
// Private blocks (<private>...</private>) must be excluded from capture.
|
|
614
|
+
const { stripPrivateBlocks } = await import("./capture.js");
|
|
615
|
+
const cleanedTexts = allTexts.map(t => ({
|
|
616
|
+
...t,
|
|
617
|
+
text: stripPrivateBlocks(
|
|
618
|
+
t.role === "user" ? stripPalaiaInjectedContext(t.text) : t.text
|
|
619
|
+
),
|
|
620
|
+
}));
|
|
570
621
|
|
|
571
622
|
// Only extract from recent exchanges — full history causes LLM timeouts
|
|
572
623
|
// and dilutes extraction quality
|
|
@@ -586,6 +637,23 @@ export function registerHooks(api: OpenClawPluginApi, config: PalaiaPluginConfig
|
|
|
586
637
|
|
|
587
638
|
const knownProjects = await loadProjects(hookOpts);
|
|
588
639
|
|
|
640
|
+
// Resolve effective capture scope from priorities (per-agent override, #147)
|
|
641
|
+
let effectiveCaptureScope = config.captureScope || "";
|
|
642
|
+
try {
|
|
643
|
+
const prio = await loadPriorities(resolved.workspace);
|
|
644
|
+
const resolvedCapturePrio = resolvePriorities(prio, {
|
|
645
|
+
recallTypeWeight: config.recallTypeWeight,
|
|
646
|
+
recallMinScore: config.recallMinScore,
|
|
647
|
+
maxInjectedChars: config.maxInjectedChars,
|
|
648
|
+
tier: config.tier,
|
|
649
|
+
}, agentName);
|
|
650
|
+
if (resolvedCapturePrio.captureScope) {
|
|
651
|
+
effectiveCaptureScope = resolvedCapturePrio.captureScope;
|
|
652
|
+
}
|
|
653
|
+
} catch {
|
|
654
|
+
// Fall through to config default
|
|
655
|
+
}
|
|
656
|
+
|
|
589
657
|
// Helper: build CLI args with metadata
|
|
590
658
|
const buildWriteArgs = (
|
|
591
659
|
content: string,
|
|
@@ -601,9 +669,9 @@ export function registerHooks(api: OpenClawPluginApi, config: PalaiaPluginConfig
|
|
|
601
669
|
"--tags", tags.join(",") || "auto-capture",
|
|
602
670
|
];
|
|
603
671
|
|
|
604
|
-
// Scope guardrail: config.captureScope
|
|
605
|
-
const scope =
|
|
606
|
-
? sanitizeScope(
|
|
672
|
+
// Scope guardrail: priorities captureScope > config.captureScope > hint/LLM scope
|
|
673
|
+
const scope = effectiveCaptureScope
|
|
674
|
+
? sanitizeScope(effectiveCaptureScope, "team", true)
|
|
607
675
|
: sanitizeScope(itemScope, "team", false);
|
|
608
676
|
args.push("--scope", scope);
|
|
609
677
|
|
|
@@ -821,7 +889,7 @@ export function registerHooks(api: OpenClawPluginApi, config: PalaiaPluginConfig
|
|
|
821
889
|
// ── Startup Recovery Service ───────────────────────────────────
|
|
822
890
|
api.registerService({
|
|
823
891
|
id: "palaia-recovery",
|
|
824
|
-
start: async () => {
|
|
892
|
+
start: async (_ctx) => {
|
|
825
893
|
const result = await recover(opts);
|
|
826
894
|
if (result.replayed > 0) {
|
|
827
895
|
logger.info(`[palaia] WAL recovery: replayed ${result.replayed} entries`);
|
package/src/hooks/recall.ts
CHANGED
|
@@ -26,6 +26,7 @@ export interface QueryResult {
|
|
|
26
26
|
title?: string;
|
|
27
27
|
type?: string;
|
|
28
28
|
tags?: string[];
|
|
29
|
+
created?: string;
|
|
29
30
|
}>;
|
|
30
31
|
}
|
|
31
32
|
|
|
@@ -342,16 +343,36 @@ export interface RankedEntry {
|
|
|
342
343
|
bm25Score?: number;
|
|
343
344
|
embedScore?: number;
|
|
344
345
|
weightedScore: number;
|
|
346
|
+
created?: string;
|
|
347
|
+
tags?: string[];
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Calculate recency boost factor.
|
|
352
|
+
* Returns a multiplier: 1.0 (no boost) to 1.0 + boostFactor (max boost for very recent).
|
|
353
|
+
* Formula: 1 + boostFactor * exp(-hoursAgo / 24)
|
|
354
|
+
*/
|
|
355
|
+
function calcRecencyBoost(created: string | undefined, boostFactor: number): number {
|
|
356
|
+
if (!boostFactor || !created) return 1.0;
|
|
357
|
+
try {
|
|
358
|
+
const hoursAgo = (Date.now() - new Date(created).getTime()) / (1000 * 60 * 60);
|
|
359
|
+
if (hoursAgo < 0 || isNaN(hoursAgo)) return 1.0;
|
|
360
|
+
return 1.0 + boostFactor * Math.exp(-hoursAgo / 24);
|
|
361
|
+
} catch {
|
|
362
|
+
return 1.0;
|
|
363
|
+
}
|
|
345
364
|
}
|
|
346
365
|
|
|
347
366
|
export function rerankByTypeWeight(
|
|
348
367
|
results: QueryResult["results"],
|
|
349
|
-
weights:
|
|
368
|
+
weights: Record<string, number>,
|
|
369
|
+
recencyBoost = 0,
|
|
350
370
|
): RankedEntry[] {
|
|
351
371
|
return results
|
|
352
372
|
.map((r) => {
|
|
353
373
|
const type = r.type || "memory";
|
|
354
374
|
const weight = weights[type] ?? 1.0;
|
|
375
|
+
const recency = calcRecencyBoost(r.created, recencyBoost);
|
|
355
376
|
return {
|
|
356
377
|
id: r.id,
|
|
357
378
|
body: r.content || r.body || "",
|
|
@@ -362,8 +383,50 @@ export function rerankByTypeWeight(
|
|
|
362
383
|
score: r.score,
|
|
363
384
|
bm25Score: r.bm25_score,
|
|
364
385
|
embedScore: r.embed_score,
|
|
365
|
-
weightedScore: r.score * weight,
|
|
386
|
+
weightedScore: r.score * weight * recency,
|
|
387
|
+
created: r.created,
|
|
388
|
+
tags: r.tags,
|
|
366
389
|
};
|
|
367
390
|
})
|
|
368
391
|
.sort((a, b) => b.weightedScore - a.weightedScore);
|
|
369
392
|
}
|
|
393
|
+
|
|
394
|
+
// ── Context Formatting ──────────────────────────────────────────────────
|
|
395
|
+
|
|
396
|
+
const SCOPE_SHORT: Record<string, string> = { team: "t", private: "p", public: "pub" };
|
|
397
|
+
const TYPE_SHORT: Record<string, string> = { memory: "m", process: "pr", task: "tk" };
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Format a ranked entry as an injectable context line.
|
|
401
|
+
*
|
|
402
|
+
* In compact mode (progressive disclosure), only title + first line + ID are shown.
|
|
403
|
+
* The agent can use `memory_get <id>` for the full entry.
|
|
404
|
+
*/
|
|
405
|
+
export function formatEntryLine(entry: RankedEntry, compact: boolean): string {
|
|
406
|
+
const scopeKey = SCOPE_SHORT[entry.scope] || entry.scope;
|
|
407
|
+
const typeKey = TYPE_SHORT[entry.type] || entry.type;
|
|
408
|
+
const prefix = `[${scopeKey}/${typeKey}]`;
|
|
409
|
+
|
|
410
|
+
if (compact) {
|
|
411
|
+
// Compact: title + first line of body + ID reference
|
|
412
|
+
const firstLine = entry.body.split("\n")[0]?.slice(0, 120) || "";
|
|
413
|
+
const titlePart = entry.body.toLowerCase().startsWith(entry.title.toLowerCase())
|
|
414
|
+
? firstLine
|
|
415
|
+
: `${entry.title} — ${firstLine}`;
|
|
416
|
+
return `${prefix} ${titlePart} [id:${entry.id}]\n`;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Full: title + complete body
|
|
420
|
+
if (entry.body.toLowerCase().startsWith(entry.title.toLowerCase())) {
|
|
421
|
+
return `${prefix} ${entry.body}\n\n`;
|
|
422
|
+
}
|
|
423
|
+
return `${prefix} ${entry.title}\n${entry.body}\n\n`;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Determine if compact mode should be used based on result count.
|
|
428
|
+
* Above threshold, use compact mode to fit more entries in budget.
|
|
429
|
+
*/
|
|
430
|
+
export function shouldUseCompactMode(totalResults: number, threshold = 100): boolean {
|
|
431
|
+
return totalResults > threshold;
|
|
432
|
+
}
|