@loreai/core 0.20.2 → 0.22.0
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/dist/bun/config.d.ts +1 -0
- package/dist/bun/config.d.ts.map +1 -1
- package/dist/bun/curator.d.ts +2 -0
- package/dist/bun/curator.d.ts.map +1 -1
- package/dist/bun/db.d.ts +13 -2
- package/dist/bun/db.d.ts.map +1 -1
- package/dist/bun/distillation.d.ts.map +1 -1
- package/dist/bun/gradient.d.ts +7 -4
- package/dist/bun/gradient.d.ts.map +1 -1
- package/dist/bun/index.d.ts +2 -2
- package/dist/bun/index.d.ts.map +1 -1
- package/dist/bun/index.js +156 -23
- package/dist/bun/index.js.map +3 -3
- package/dist/bun/ltm.d.ts +10 -0
- package/dist/bun/ltm.d.ts.map +1 -1
- package/dist/bun/prompt.d.ts +1 -1
- package/dist/bun/prompt.d.ts.map +1 -1
- package/dist/node/config.d.ts +1 -0
- package/dist/node/config.d.ts.map +1 -1
- package/dist/node/curator.d.ts +2 -0
- package/dist/node/curator.d.ts.map +1 -1
- package/dist/node/db.d.ts +13 -2
- package/dist/node/db.d.ts.map +1 -1
- package/dist/node/distillation.d.ts.map +1 -1
- package/dist/node/gradient.d.ts +7 -4
- package/dist/node/gradient.d.ts.map +1 -1
- package/dist/node/index.d.ts +2 -2
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +156 -23
- package/dist/node/index.js.map +3 -3
- package/dist/node/ltm.d.ts +10 -0
- package/dist/node/ltm.d.ts.map +1 -1
- package/dist/node/prompt.d.ts +1 -1
- package/dist/node/prompt.d.ts.map +1 -1
- package/dist/types/config.d.ts +1 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/curator.d.ts +2 -0
- package/dist/types/curator.d.ts.map +1 -1
- package/dist/types/db.d.ts +13 -2
- package/dist/types/db.d.ts.map +1 -1
- package/dist/types/distillation.d.ts.map +1 -1
- package/dist/types/gradient.d.ts +7 -4
- package/dist/types/gradient.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/ltm.d.ts +10 -0
- package/dist/types/ltm.d.ts.map +1 -1
- package/dist/types/prompt.d.ts +1 -1
- package/dist/types/prompt.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/config.ts +4 -2
- package/src/curator.ts +3 -0
- package/src/db.ts +66 -14
- package/src/distillation.ts +5 -0
- package/src/gradient.ts +27 -12
- package/src/index.ts +2 -0
- package/src/ltm.ts +101 -5
- package/src/prompt.ts +31 -4
package/src/gradient.ts
CHANGED
|
@@ -135,10 +135,11 @@ export function getTier(tokens: number): number {
|
|
|
135
135
|
*
|
|
136
136
|
* A "bust" is when cache_write > 50% of total input tokens.
|
|
137
137
|
*
|
|
138
|
-
* @param cacheWrite
|
|
139
|
-
* @param cacheRead
|
|
140
|
-
* @param inputTokens -
|
|
141
|
-
*
|
|
138
|
+
* @param cacheWrite - cache_creation_input_tokens from the API response
|
|
139
|
+
* @param cacheRead - cache_read_input_tokens from the API response
|
|
140
|
+
* @param inputTokens - input_tokens from the API response (uncached portion only —
|
|
141
|
+
* Anthropic's input_tokens excludes both cache reads and writes)
|
|
142
|
+
* @param sessionID - session that produced this response
|
|
142
143
|
*/
|
|
143
144
|
export function recordCacheUsage(
|
|
144
145
|
cacheWrite: number,
|
|
@@ -149,16 +150,25 @@ export function recordCacheUsage(
|
|
|
149
150
|
if (!sessionID) return;
|
|
150
151
|
const state = getSessionState(sessionID);
|
|
151
152
|
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
const total =
|
|
153
|
+
// Total = cacheWrite + cacheRead + uncached input. Anthropic's input_tokens
|
|
154
|
+
// field is only the uncached portion, NOT the total — using it alone as the
|
|
155
|
+
// denominator makes every cached turn look like a bust (e.g. 1000/3 >> 0.5).
|
|
156
|
+
const total = cacheWrite + cacheRead + inputTokens;
|
|
156
157
|
if (total > 0) {
|
|
157
|
-
|
|
158
|
+
const bustRatio = cacheWrite / total;
|
|
159
|
+
const prev = state.consecutiveBusts;
|
|
160
|
+
if (bustRatio > 0.5) {
|
|
158
161
|
state.consecutiveBusts++;
|
|
159
162
|
} else {
|
|
160
163
|
state.consecutiveBusts = 0;
|
|
161
164
|
}
|
|
165
|
+
if (state.consecutiveBusts !== prev) {
|
|
166
|
+
log.info(
|
|
167
|
+
`bust-tracker: session=${sessionID.slice(0, 16)} ratio=${bustRatio.toFixed(3)}` +
|
|
168
|
+
` (write=${cacheWrite} read=${cacheRead} uncached=${inputTokens})` +
|
|
169
|
+
` busts=${prev}→${state.consecutiveBusts}`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
162
172
|
}
|
|
163
173
|
}
|
|
164
174
|
|
|
@@ -316,9 +326,11 @@ function getSessionState(sessionID: string): SessionState {
|
|
|
316
326
|
state.lastLayer = persisted.lastLayer as SafetyLayer;
|
|
317
327
|
state.lastKnownInput = persisted.lastKnownInput;
|
|
318
328
|
state.lastTurnAt = persisted.lastTurnAt;
|
|
319
|
-
// consecutiveBusts
|
|
320
|
-
//
|
|
321
|
-
state
|
|
329
|
+
// Don't restore consecutiveBusts from DB — it's a short-term rolling
|
|
330
|
+
// signal that must rebuild from live API responses in the current process.
|
|
331
|
+
// Stale values from a previous process (different cache state after restart)
|
|
332
|
+
// cause false unsustainable warnings. The dynamicContextCap column is still
|
|
333
|
+
// written for diagnostics but not consumed on restore.
|
|
322
334
|
}
|
|
323
335
|
|
|
324
336
|
sessionStates.set(sessionID, state);
|
|
@@ -475,6 +487,9 @@ export function getLtmBudget(ltmFraction: number): number {
|
|
|
475
487
|
return Math.floor(usable * ltmFraction);
|
|
476
488
|
}
|
|
477
489
|
|
|
490
|
+
/** Returns the token budget for stable LTM (preferences). Independent of context-bound LTM budget. */
|
|
491
|
+
export const getPreferenceLtmBudget = getLtmBudget;
|
|
492
|
+
|
|
478
493
|
// Called after each assistant message completes with real token usage data.
|
|
479
494
|
// actualInput = tokens.input + tokens.cache.read + tokens.cache.write
|
|
480
495
|
// sessionID = session that produced this response (for exact-tracking validity)
|
package/src/index.ts
CHANGED
|
@@ -78,6 +78,7 @@ export {
|
|
|
78
78
|
saveSessionTracking,
|
|
79
79
|
loadSessionTracking,
|
|
80
80
|
loadHeaderSessionIndex,
|
|
81
|
+
loadParentChildMap,
|
|
81
82
|
type SessionTrackingState,
|
|
82
83
|
type LoadedSessionTracking,
|
|
83
84
|
getKV,
|
|
@@ -104,6 +105,7 @@ export {
|
|
|
104
105
|
setLtmTokens,
|
|
105
106
|
getLtmTokens,
|
|
106
107
|
getLtmBudget,
|
|
108
|
+
getPreferenceLtmBudget,
|
|
107
109
|
setForceMinLayer,
|
|
108
110
|
getLastTransformedCount,
|
|
109
111
|
getLastTransformEstimate,
|
package/src/ltm.ts
CHANGED
|
@@ -44,6 +44,8 @@ export function create(input: {
|
|
|
44
44
|
crossProject?: boolean;
|
|
45
45
|
/** Explicit ID to use — for cross-machine import via agents-file. Defaults to a new UUIDv7. */
|
|
46
46
|
id?: string;
|
|
47
|
+
/** Initial confidence (0.0–1.0). Default 1.0. Controls injection priority for preferences. */
|
|
48
|
+
confidence?: number;
|
|
47
49
|
}): string {
|
|
48
50
|
const pid =
|
|
49
51
|
input.scope === "project" && input.projectPath
|
|
@@ -77,8 +79,15 @@ export function create(input: {
|
|
|
77
79
|
.get(input.title)
|
|
78
80
|
) as { id: string } | null;
|
|
79
81
|
|
|
82
|
+
// Build the update payload — forward confidence when the caller provided one
|
|
83
|
+
// so the curator's scoring intent isn't silently dropped on dedup.
|
|
84
|
+
const dedupUpdate = {
|
|
85
|
+
content: input.content,
|
|
86
|
+
...(input.confidence != null ? { confidence: input.confidence } : {}),
|
|
87
|
+
};
|
|
88
|
+
|
|
80
89
|
if (existing) {
|
|
81
|
-
update(existing.id,
|
|
90
|
+
update(existing.id, dedupUpdate);
|
|
82
91
|
return existing.id;
|
|
83
92
|
}
|
|
84
93
|
|
|
@@ -91,7 +100,7 @@ export function create(input: {
|
|
|
91
100
|
.get(input.title) as { id: string } | null;
|
|
92
101
|
|
|
93
102
|
if (crossExisting) {
|
|
94
|
-
update(crossExisting.id,
|
|
103
|
+
update(crossExisting.id, dedupUpdate);
|
|
95
104
|
return crossExisting.id;
|
|
96
105
|
}
|
|
97
106
|
|
|
@@ -101,17 +110,20 @@ export function create(input: {
|
|
|
101
110
|
// lock re-entry bug"). Placed after exact checks (cheaper checks first).
|
|
102
111
|
const fuzzyMatch = findFuzzyDuplicate({ title: input.title, projectId: pid });
|
|
103
112
|
if (fuzzyMatch) {
|
|
104
|
-
update(fuzzyMatch.id,
|
|
113
|
+
update(fuzzyMatch.id, dedupUpdate);
|
|
105
114
|
return fuzzyMatch.id;
|
|
106
115
|
}
|
|
107
116
|
}
|
|
108
117
|
|
|
109
118
|
const id = input.id ?? uuidv7();
|
|
110
119
|
const now = Date.now();
|
|
120
|
+
const confidence = input.confidence != null
|
|
121
|
+
? Math.max(0, Math.min(1, input.confidence))
|
|
122
|
+
: 1.0;
|
|
111
123
|
db()
|
|
112
124
|
.query(
|
|
113
125
|
`INSERT INTO knowledge (id, project_id, category, title, content, source_session, cross_project, confidence, created_at, updated_at)
|
|
114
|
-
VALUES (?, ?, ?, ?, ?, ?, ?,
|
|
126
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
115
127
|
)
|
|
116
128
|
.run(
|
|
117
129
|
id,
|
|
@@ -121,6 +133,7 @@ export function create(input: {
|
|
|
121
133
|
input.content,
|
|
122
134
|
input.session ?? null,
|
|
123
135
|
crossProject ? 1 : 0,
|
|
136
|
+
confidence,
|
|
124
137
|
now,
|
|
125
138
|
now,
|
|
126
139
|
);
|
|
@@ -440,6 +453,31 @@ export async function forSession(
|
|
|
440
453
|
|
|
441
454
|
if (!crossEntries.length && !projectEntries.length) return [];
|
|
442
455
|
|
|
456
|
+
// --- Preference-only fast path ---
|
|
457
|
+
// Preferences are unconditional user directives — relevance scoring harms them.
|
|
458
|
+
// Skip scoring; rank purely by confidence (set by curator or `lore data rerank`)
|
|
459
|
+
// then recency. Confidence carries real meaning now: 1.0 = unconditional
|
|
460
|
+
// directive, 0.9 = strong preference, 0.8 = moderate, 0.6 = mild.
|
|
461
|
+
const isPreferenceOnly = categoryFilter?.length === 1 && categoryFilter[0] === "preference";
|
|
462
|
+
if (isPreferenceOnly) {
|
|
463
|
+
const allPrefs = [...projectEntries, ...crossEntries];
|
|
464
|
+
allPrefs.sort((a, b) =>
|
|
465
|
+
a.confidence !== b.confidence ? b.confidence - a.confidence : b.updated_at - a.updated_at
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
const HEADER_OVERHEAD_TOKENS = 15;
|
|
469
|
+
let used = HEADER_OVERHEAD_TOKENS;
|
|
470
|
+
const result: KnowledgeEntry[] = [];
|
|
471
|
+
for (const entry of allPrefs) {
|
|
472
|
+
if (used >= maxTokens) break;
|
|
473
|
+
const cost = estimateTokens(entry.title + entry.content) + 10;
|
|
474
|
+
if (used + cost > maxTokens) continue;
|
|
475
|
+
result.push(entry);
|
|
476
|
+
used += cost;
|
|
477
|
+
}
|
|
478
|
+
return result;
|
|
479
|
+
}
|
|
480
|
+
|
|
443
481
|
// --- 3. Build session context for relevance scoring ---
|
|
444
482
|
let sessionContext = "";
|
|
445
483
|
if (sessionID) {
|
|
@@ -551,16 +589,38 @@ export async function forSession(
|
|
|
551
589
|
.map((entry) => ({ entry, score: entry.confidence }));
|
|
552
590
|
}
|
|
553
591
|
|
|
554
|
-
// --- 5. Merge and pack into token budget
|
|
592
|
+
// --- 5. Merge and pack into token budget ---
|
|
593
|
+
// Architecture entries get a guaranteed minimum allocation (first 20% of
|
|
594
|
+
// budget) before the general score-ranked packing. These entries provide
|
|
595
|
+
// the structural "map" that makes specific gotchas/decisions interpretable
|
|
596
|
+
// — without them, a gotcha about a subsystem is harder to contextualize.
|
|
555
597
|
const allScored = [...scoredProject, ...scoredCross];
|
|
556
598
|
allScored.sort((a, b) => b.score - a.score);
|
|
557
599
|
|
|
558
600
|
const HEADER_OVERHEAD_TOKENS = 15;
|
|
601
|
+
const ARCH_BUDGET_FRACTION = 0.2;
|
|
559
602
|
let used = HEADER_OVERHEAD_TOKENS;
|
|
560
603
|
const result: KnowledgeEntry[] = [];
|
|
604
|
+
const packedIds = new Set<string>();
|
|
605
|
+
|
|
606
|
+
// Phase 1: Pack architecture entries first (up to 20% of budget)
|
|
607
|
+
const archBudget = Math.floor(maxTokens * ARCH_BUDGET_FRACTION);
|
|
608
|
+
const archEntries = allScored.filter((s) => s.entry.category === "architecture");
|
|
609
|
+
// Sort architecture by score descending (already sorted, but filter may reorder)
|
|
610
|
+
archEntries.sort((a, b) => b.score - a.score);
|
|
611
|
+
for (const { entry } of archEntries) {
|
|
612
|
+
if (used >= archBudget + HEADER_OVERHEAD_TOKENS) break;
|
|
613
|
+
const cost = estimateTokens(entry.title + entry.content) + 10;
|
|
614
|
+
if (used + cost > maxTokens) continue; // hard cap: never exceed total budget
|
|
615
|
+
result.push(entry);
|
|
616
|
+
packedIds.add(entry.id);
|
|
617
|
+
used += cost;
|
|
618
|
+
}
|
|
561
619
|
|
|
620
|
+
// Phase 2: Pack remaining entries by score descending (skip already packed)
|
|
562
621
|
for (const { entry } of allScored) {
|
|
563
622
|
if (used >= maxTokens) break;
|
|
623
|
+
if (packedIds.has(entry.id)) continue;
|
|
564
624
|
const cost = estimateTokens(entry.title + entry.content) + 10;
|
|
565
625
|
if (used + cost > maxTokens) continue;
|
|
566
626
|
result.push(entry);
|
|
@@ -651,6 +711,42 @@ export function crossProject(): KnowledgeEntry[] {
|
|
|
651
711
|
.all() as KnowledgeEntry[];
|
|
652
712
|
}
|
|
653
713
|
|
|
714
|
+
/**
|
|
715
|
+
* Re-score confidence on preference entries using directive-detection patterns.
|
|
716
|
+
* Only touches entries with confidence = 1.0 (legacy/unscored). Entries already
|
|
717
|
+
* scored by the curator (confidence < 1.0) are left untouched.
|
|
718
|
+
*
|
|
719
|
+
* @returns Count of entries updated.
|
|
720
|
+
*/
|
|
721
|
+
export function rerankPreferences(): number {
|
|
722
|
+
const prefs = db()
|
|
723
|
+
.query(`SELECT ${KNOWLEDGE_COLS} FROM knowledge WHERE category = 'preference' AND confidence = 1.0`)
|
|
724
|
+
.all() as KnowledgeEntry[];
|
|
725
|
+
|
|
726
|
+
// Strong unconditional directives
|
|
727
|
+
const STRONG_DIRECTIVE_RE = /\b(never|always|must not|must)\b/i;
|
|
728
|
+
// Explicit preference language
|
|
729
|
+
const EXPLICIT_PREF_RE = /\b(I (?:want|need|prefer|expect)|make sure to|don'?t forget)\b/i;
|
|
730
|
+
|
|
731
|
+
let updated = 0;
|
|
732
|
+
for (const entry of prefs) {
|
|
733
|
+
const text = entry.title + " " + entry.content;
|
|
734
|
+
let newConfidence: number;
|
|
735
|
+
if (STRONG_DIRECTIVE_RE.test(text)) {
|
|
736
|
+
newConfidence = 1.0; // Keep at max — unconditional directive
|
|
737
|
+
} else if (EXPLICIT_PREF_RE.test(text)) {
|
|
738
|
+
newConfidence = 0.9; // Strong but not absolute
|
|
739
|
+
} else {
|
|
740
|
+
newConfidence = 0.8; // No directive language detected — moderate
|
|
741
|
+
}
|
|
742
|
+
if (newConfidence !== entry.confidence) {
|
|
743
|
+
update(entry.id, { confidence: newConfidence });
|
|
744
|
+
updated++;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return updated;
|
|
748
|
+
}
|
|
749
|
+
|
|
654
750
|
// LIKE-based fallback for when FTS5 fails unexpectedly.
|
|
655
751
|
function searchLike(input: {
|
|
656
752
|
query: string;
|
package/src/prompt.ts
CHANGED
|
@@ -218,7 +218,9 @@ export const CURATOR_SYSTEM = `You are a long-term memory curator. Your job is t
|
|
|
218
218
|
Focus ONLY on knowledge that helps a coding agent work effectively on THIS codebase:
|
|
219
219
|
- Architectural decisions and their rationale (why something was built a certain way)
|
|
220
220
|
- Non-obvious implementation patterns and conventions specific to the project
|
|
221
|
-
- Recurring gotchas, constraints, or traps in the codebase
|
|
221
|
+
- Recurring gotchas, constraints, or traps in the codebase — always include WHY the
|
|
222
|
+
wrong approach seems right, not just the trap and fix. Without this, a future session
|
|
223
|
+
will re-propose the broken approach because it looks like a reasonable improvement.
|
|
222
224
|
- Environment/tooling setup details that affect development
|
|
223
225
|
- Important relationships between components that aren't obvious from reading the code
|
|
224
226
|
- User preferences and working style specific to how they use this project
|
|
@@ -237,10 +239,19 @@ Do NOT extract:
|
|
|
237
239
|
- Knowledge about unrelated projects or repositories unless explicitly cross-project
|
|
238
240
|
- Restatements of what the code obviously does (e.g. "the auth module handles authentication")
|
|
239
241
|
|
|
242
|
+
INCLUDE THE "WHY" — decisions and gotchas without rationale get undone:
|
|
243
|
+
- Every "decision" MUST include the rejected alternative and why it was rejected.
|
|
244
|
+
Format: "Chose X over Y because Z." Without the rejected option, a future session
|
|
245
|
+
will re-propose Y because it looks like a reasonable improvement.
|
|
246
|
+
- Every "gotcha" MUST explain why the wrong approach seems correct, not just the trap
|
|
247
|
+
and its fix. Format: "Trap: X looks right because [reason]. Fix: Y, because [reason]."
|
|
248
|
+
- Any standard or rule without its rationale is vulnerable to being optimized away by
|
|
249
|
+
a session that doesn't know what problem it was solving.
|
|
250
|
+
|
|
240
251
|
BREVITY IS CRITICAL — each entry must be concise:
|
|
241
252
|
- content MUST be under 150 words (~600 characters). Capture ONE specific actionable
|
|
242
253
|
insight in 2-3 sentences. Prefer terse technical language.
|
|
243
|
-
- Each "gotcha": one specific trap + its fix in
|
|
254
|
+
- Each "gotcha": one specific trap + WHY it looks right + its fix in 2-3 sentences
|
|
244
255
|
- Each "architecture": one design decision and its key constraint
|
|
245
256
|
- Focus on the actionable insight, not the full story behind it
|
|
246
257
|
- If a pattern requires more detail, split into multiple focused entries (each under 150 words)
|
|
@@ -266,6 +277,20 @@ crossProject flag:
|
|
|
266
277
|
- Default is true — most useful knowledge is worth sharing across projects
|
|
267
278
|
- Set crossProject to false for things that are meaningless outside this specific repo (e.g. a config path, a project-local naming convention that conflicts with your usual style)
|
|
268
279
|
|
|
280
|
+
Confidence values (0.0–1.0) — determines injection priority when budget is tight:
|
|
281
|
+
- 1.0: Unconditional directive — user used "NEVER", "ALWAYS", "from now on", or similarly
|
|
282
|
+
absolute language. These must always be respected regardless of context.
|
|
283
|
+
- 0.9: Strong preference — explicit user preference ("I prefer", "I want", "make sure to",
|
|
284
|
+
"don't forget to"). Clear intent but not absolute.
|
|
285
|
+
- 0.8: Moderate preference — inferred from repeated user behavior or gentle correction across
|
|
286
|
+
sessions. Not explicitly stated as a rule.
|
|
287
|
+
- 0.6: Mild/contextual preference — may not apply universally. Observed once or context-dependent.
|
|
288
|
+
- For non-preference categories (gotcha, pattern, architecture, decision), confidence reflects
|
|
289
|
+
how well-established the knowledge is: 1.0 = verified/confirmed, 0.8 = high confidence,
|
|
290
|
+
0.6 = probable but unverified.
|
|
291
|
+
- Default to 1.0 for preferences with strong directive language, 0.8 for other preferences.
|
|
292
|
+
- Always set confidence on create ops — it determines injection priority.
|
|
293
|
+
|
|
269
294
|
Produce a JSON array of operations:
|
|
270
295
|
[
|
|
271
296
|
{
|
|
@@ -274,7 +299,8 @@ Produce a JSON array of operations:
|
|
|
274
299
|
"title": "Short descriptive title",
|
|
275
300
|
"content": "Concise knowledge entry — under 150 words",
|
|
276
301
|
"scope": "project" | "global",
|
|
277
|
-
"crossProject": true
|
|
302
|
+
"crossProject": true,
|
|
303
|
+
"confidence": 1.0
|
|
278
304
|
},
|
|
279
305
|
{
|
|
280
306
|
"op": "update",
|
|
@@ -322,7 +348,8 @@ IMPORTANT:
|
|
|
322
348
|
4. Only create a new entry for genuinely distinct knowledge with no existing home.
|
|
323
349
|
5. Keep all entries under 150 words. If an existing entry is too long, use an update op to trim it.
|
|
324
350
|
6. Pay special attention to user instructions ("always do X", "never do Y", "make sure to X").
|
|
325
|
-
These are strong signals for "preference" entries with high confidence
|
|
351
|
+
These are strong signals for "preference" entries with high confidence (1.0 for absolute
|
|
352
|
+
directives like "never"/"always", 0.9 for explicit preferences like "I prefer").`;
|
|
326
353
|
}
|
|
327
354
|
|
|
328
355
|
/**
|