@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.
Files changed (59) hide show
  1. package/README.md +1 -1
  2. package/dist/bun/config.d.ts +1 -0
  3. package/dist/bun/config.d.ts.map +1 -1
  4. package/dist/bun/curator.d.ts +2 -0
  5. package/dist/bun/curator.d.ts.map +1 -1
  6. package/dist/bun/db.d.ts +13 -2
  7. package/dist/bun/db.d.ts.map +1 -1
  8. package/dist/bun/distillation.d.ts.map +1 -1
  9. package/dist/bun/gradient.d.ts +7 -4
  10. package/dist/bun/gradient.d.ts.map +1 -1
  11. package/dist/bun/index.d.ts +2 -2
  12. package/dist/bun/index.d.ts.map +1 -1
  13. package/dist/bun/index.js +156 -23
  14. package/dist/bun/index.js.map +3 -3
  15. package/dist/bun/ltm.d.ts +10 -0
  16. package/dist/bun/ltm.d.ts.map +1 -1
  17. package/dist/bun/prompt.d.ts +1 -1
  18. package/dist/bun/prompt.d.ts.map +1 -1
  19. package/dist/node/config.d.ts +1 -0
  20. package/dist/node/config.d.ts.map +1 -1
  21. package/dist/node/curator.d.ts +2 -0
  22. package/dist/node/curator.d.ts.map +1 -1
  23. package/dist/node/db.d.ts +13 -2
  24. package/dist/node/db.d.ts.map +1 -1
  25. package/dist/node/distillation.d.ts.map +1 -1
  26. package/dist/node/gradient.d.ts +7 -4
  27. package/dist/node/gradient.d.ts.map +1 -1
  28. package/dist/node/index.d.ts +2 -2
  29. package/dist/node/index.d.ts.map +1 -1
  30. package/dist/node/index.js +156 -23
  31. package/dist/node/index.js.map +3 -3
  32. package/dist/node/ltm.d.ts +10 -0
  33. package/dist/node/ltm.d.ts.map +1 -1
  34. package/dist/node/prompt.d.ts +1 -1
  35. package/dist/node/prompt.d.ts.map +1 -1
  36. package/dist/types/config.d.ts +1 -0
  37. package/dist/types/config.d.ts.map +1 -1
  38. package/dist/types/curator.d.ts +2 -0
  39. package/dist/types/curator.d.ts.map +1 -1
  40. package/dist/types/db.d.ts +13 -2
  41. package/dist/types/db.d.ts.map +1 -1
  42. package/dist/types/distillation.d.ts.map +1 -1
  43. package/dist/types/gradient.d.ts +7 -4
  44. package/dist/types/gradient.d.ts.map +1 -1
  45. package/dist/types/index.d.ts +2 -2
  46. package/dist/types/index.d.ts.map +1 -1
  47. package/dist/types/ltm.d.ts +10 -0
  48. package/dist/types/ltm.d.ts.map +1 -1
  49. package/dist/types/prompt.d.ts +1 -1
  50. package/dist/types/prompt.d.ts.map +1 -1
  51. package/package.json +1 -1
  52. package/src/config.ts +4 -2
  53. package/src/curator.ts +3 -0
  54. package/src/db.ts +66 -14
  55. package/src/distillation.ts +5 -0
  56. package/src/gradient.ts +27 -12
  57. package/src/index.ts +2 -0
  58. package/src/ltm.ts +101 -5
  59. 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 - cache_creation_input_tokens from the API response
139
- * @param cacheRead - cache_read_input_tokens from the API response
140
- * @param inputTokens - total input_tokens from the API response (includes uncached)
141
- * @param sessionID - session that produced this response
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
- // Use total input tokens as denominator (includes uncached input),
153
- // not just cacheWrite + cacheRead, to avoid inflated bust ratios
154
- // when a large fraction of tokens is uncached.
155
- const total = inputTokens > 0 ? inputTokens : cacheWrite + cacheRead;
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
- if (cacheWrite / total > 0.5) {
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 is persisted in the dynamicContextCap column
320
- // (repurposed, see saveGradientState).
321
- state.consecutiveBusts = persisted.dynamicContextCap;
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, { content: input.content });
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, { content: input.content });
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, { content: input.content });
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 (?, ?, ?, ?, ?, ?, ?, 1.0, ?, ?)`,
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 by score descending ---
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 1-2 sentences
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
  /**