@loreai/core 0.20.2 → 0.21.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 (58) hide show
  1. package/dist/bun/config.d.ts +1 -0
  2. package/dist/bun/config.d.ts.map +1 -1
  3. package/dist/bun/curator.d.ts +2 -0
  4. package/dist/bun/curator.d.ts.map +1 -1
  5. package/dist/bun/db.d.ts +10 -0
  6. package/dist/bun/db.d.ts.map +1 -1
  7. package/dist/bun/distillation.d.ts.map +1 -1
  8. package/dist/bun/gradient.d.ts +7 -4
  9. package/dist/bun/gradient.d.ts.map +1 -1
  10. package/dist/bun/index.d.ts +2 -2
  11. package/dist/bun/index.d.ts.map +1 -1
  12. package/dist/bun/index.js +124 -14
  13. package/dist/bun/index.js.map +3 -3
  14. package/dist/bun/ltm.d.ts +10 -0
  15. package/dist/bun/ltm.d.ts.map +1 -1
  16. package/dist/bun/prompt.d.ts +1 -1
  17. package/dist/bun/prompt.d.ts.map +1 -1
  18. package/dist/node/config.d.ts +1 -0
  19. package/dist/node/config.d.ts.map +1 -1
  20. package/dist/node/curator.d.ts +2 -0
  21. package/dist/node/curator.d.ts.map +1 -1
  22. package/dist/node/db.d.ts +10 -0
  23. package/dist/node/db.d.ts.map +1 -1
  24. package/dist/node/distillation.d.ts.map +1 -1
  25. package/dist/node/gradient.d.ts +7 -4
  26. package/dist/node/gradient.d.ts.map +1 -1
  27. package/dist/node/index.d.ts +2 -2
  28. package/dist/node/index.d.ts.map +1 -1
  29. package/dist/node/index.js +124 -14
  30. package/dist/node/index.js.map +3 -3
  31. package/dist/node/ltm.d.ts +10 -0
  32. package/dist/node/ltm.d.ts.map +1 -1
  33. package/dist/node/prompt.d.ts +1 -1
  34. package/dist/node/prompt.d.ts.map +1 -1
  35. package/dist/types/config.d.ts +1 -0
  36. package/dist/types/config.d.ts.map +1 -1
  37. package/dist/types/curator.d.ts +2 -0
  38. package/dist/types/curator.d.ts.map +1 -1
  39. package/dist/types/db.d.ts +10 -0
  40. package/dist/types/db.d.ts.map +1 -1
  41. package/dist/types/distillation.d.ts.map +1 -1
  42. package/dist/types/gradient.d.ts +7 -4
  43. package/dist/types/gradient.d.ts.map +1 -1
  44. package/dist/types/index.d.ts +2 -2
  45. package/dist/types/index.d.ts.map +1 -1
  46. package/dist/types/ltm.d.ts +10 -0
  47. package/dist/types/ltm.d.ts.map +1 -1
  48. package/dist/types/prompt.d.ts +1 -1
  49. package/dist/types/prompt.d.ts.map +1 -1
  50. package/package.json +1 -1
  51. package/src/config.ts +4 -2
  52. package/src/curator.ts +3 -0
  53. package/src/db.ts +52 -1
  54. package/src/distillation.ts +5 -0
  55. package/src/gradient.ts +27 -12
  56. package/src/index.ts +2 -0
  57. package/src/ltm.ts +78 -4
  58. package/src/prompt.ts +18 -2
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) {
@@ -651,6 +689,42 @@ export function crossProject(): KnowledgeEntry[] {
651
689
  .all() as KnowledgeEntry[];
652
690
  }
653
691
 
692
+ /**
693
+ * Re-score confidence on preference entries using directive-detection patterns.
694
+ * Only touches entries with confidence = 1.0 (legacy/unscored). Entries already
695
+ * scored by the curator (confidence < 1.0) are left untouched.
696
+ *
697
+ * @returns Count of entries updated.
698
+ */
699
+ export function rerankPreferences(): number {
700
+ const prefs = db()
701
+ .query(`SELECT ${KNOWLEDGE_COLS} FROM knowledge WHERE category = 'preference' AND confidence = 1.0`)
702
+ .all() as KnowledgeEntry[];
703
+
704
+ // Strong unconditional directives
705
+ const STRONG_DIRECTIVE_RE = /\b(never|always|must not|must)\b/i;
706
+ // Explicit preference language
707
+ const EXPLICIT_PREF_RE = /\b(I (?:want|need|prefer|expect)|make sure to|don'?t forget)\b/i;
708
+
709
+ let updated = 0;
710
+ for (const entry of prefs) {
711
+ const text = entry.title + " " + entry.content;
712
+ let newConfidence: number;
713
+ if (STRONG_DIRECTIVE_RE.test(text)) {
714
+ newConfidence = 1.0; // Keep at max — unconditional directive
715
+ } else if (EXPLICIT_PREF_RE.test(text)) {
716
+ newConfidence = 0.9; // Strong but not absolute
717
+ } else {
718
+ newConfidence = 0.8; // No directive language detected — moderate
719
+ }
720
+ if (newConfidence !== entry.confidence) {
721
+ update(entry.id, { confidence: newConfidence });
722
+ updated++;
723
+ }
724
+ }
725
+ return updated;
726
+ }
727
+
654
728
  // LIKE-based fallback for when FTS5 fails unexpectedly.
655
729
  function searchLike(input: {
656
730
  query: string;
package/src/prompt.ts CHANGED
@@ -266,6 +266,20 @@ crossProject flag:
266
266
  - Default is true — most useful knowledge is worth sharing across projects
267
267
  - 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
268
 
269
+ Confidence values (0.0–1.0) — determines injection priority when budget is tight:
270
+ - 1.0: Unconditional directive — user used "NEVER", "ALWAYS", "from now on", or similarly
271
+ absolute language. These must always be respected regardless of context.
272
+ - 0.9: Strong preference — explicit user preference ("I prefer", "I want", "make sure to",
273
+ "don't forget to"). Clear intent but not absolute.
274
+ - 0.8: Moderate preference — inferred from repeated user behavior or gentle correction across
275
+ sessions. Not explicitly stated as a rule.
276
+ - 0.6: Mild/contextual preference — may not apply universally. Observed once or context-dependent.
277
+ - For non-preference categories (gotcha, pattern, architecture, decision), confidence reflects
278
+ how well-established the knowledge is: 1.0 = verified/confirmed, 0.8 = high confidence,
279
+ 0.6 = probable but unverified.
280
+ - Default to 1.0 for preferences with strong directive language, 0.8 for other preferences.
281
+ - Always set confidence on create ops — it determines injection priority.
282
+
269
283
  Produce a JSON array of operations:
270
284
  [
271
285
  {
@@ -274,7 +288,8 @@ Produce a JSON array of operations:
274
288
  "title": "Short descriptive title",
275
289
  "content": "Concise knowledge entry — under 150 words",
276
290
  "scope": "project" | "global",
277
- "crossProject": true
291
+ "crossProject": true,
292
+ "confidence": 1.0
278
293
  },
279
294
  {
280
295
  "op": "update",
@@ -322,7 +337,8 @@ IMPORTANT:
322
337
  4. Only create a new entry for genuinely distinct knowledge with no existing home.
323
338
  5. Keep all entries under 150 words. If an existing entry is too long, use an update op to trim it.
324
339
  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.`;
340
+ These are strong signals for "preference" entries with high confidence (1.0 for absolute
341
+ directives like "never"/"always", 0.9 for explicit preferences like "I prefer").`;
326
342
  }
327
343
 
328
344
  /**