@loreai/core 0.17.1 → 0.19.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 (248) hide show
  1. package/dist/bun/agents-file.d.ts +4 -0
  2. package/dist/bun/agents-file.d.ts.map +1 -1
  3. package/dist/bun/config.d.ts +2 -0
  4. package/dist/bun/config.d.ts.map +1 -1
  5. package/dist/bun/curator.d.ts +45 -0
  6. package/dist/bun/curator.d.ts.map +1 -1
  7. package/dist/bun/data-dir.d.ts +18 -0
  8. package/dist/bun/data-dir.d.ts.map +1 -0
  9. package/dist/bun/db.d.ts +85 -0
  10. package/dist/bun/db.d.ts.map +1 -1
  11. package/dist/bun/distillation.d.ts +2 -13
  12. package/dist/bun/distillation.d.ts.map +1 -1
  13. package/dist/bun/embedding-vendor.d.ts +22 -38
  14. package/dist/bun/embedding-vendor.d.ts.map +1 -1
  15. package/dist/bun/embedding-worker-types.d.ts +17 -12
  16. package/dist/bun/embedding-worker-types.d.ts.map +1 -1
  17. package/dist/bun/embedding-worker.d.ts +9 -2
  18. package/dist/bun/embedding-worker.d.ts.map +1 -1
  19. package/dist/bun/embedding-worker.js +38864 -33
  20. package/dist/bun/embedding-worker.js.map +4 -4
  21. package/dist/bun/embedding.d.ts +35 -23
  22. package/dist/bun/embedding.d.ts.map +1 -1
  23. package/dist/bun/gradient.d.ts +17 -1
  24. package/dist/bun/gradient.d.ts.map +1 -1
  25. package/dist/bun/import/detect.d.ts +14 -0
  26. package/dist/bun/import/detect.d.ts.map +1 -0
  27. package/dist/bun/import/extract.d.ts +43 -0
  28. package/dist/bun/import/extract.d.ts.map +1 -0
  29. package/dist/bun/import/history.d.ts +40 -0
  30. package/dist/bun/import/history.d.ts.map +1 -0
  31. package/dist/bun/import/index.d.ts +17 -0
  32. package/dist/bun/import/index.d.ts.map +1 -0
  33. package/dist/bun/import/providers/aider.d.ts +2 -0
  34. package/dist/bun/import/providers/aider.d.ts.map +1 -0
  35. package/dist/bun/import/providers/claude-code.d.ts +2 -0
  36. package/dist/bun/import/providers/claude-code.d.ts.map +1 -0
  37. package/dist/bun/import/providers/cline.d.ts +2 -0
  38. package/dist/bun/import/providers/cline.d.ts.map +1 -0
  39. package/dist/bun/import/providers/codex.d.ts +2 -0
  40. package/dist/bun/import/providers/codex.d.ts.map +1 -0
  41. package/dist/bun/import/providers/continue.d.ts +2 -0
  42. package/dist/bun/import/providers/continue.d.ts.map +1 -0
  43. package/dist/bun/import/providers/index.d.ts +19 -0
  44. package/dist/bun/import/providers/index.d.ts.map +1 -0
  45. package/dist/bun/import/providers/opencode.d.ts +2 -0
  46. package/dist/bun/import/providers/opencode.d.ts.map +1 -0
  47. package/dist/bun/import/providers/pi.d.ts +2 -0
  48. package/dist/bun/import/providers/pi.d.ts.map +1 -0
  49. package/dist/bun/import/types.d.ts +82 -0
  50. package/dist/bun/import/types.d.ts.map +1 -0
  51. package/dist/bun/index.d.ts +5 -2
  52. package/dist/bun/index.d.ts.map +1 -1
  53. package/dist/bun/index.js +3150 -439
  54. package/dist/bun/index.js.map +4 -4
  55. package/dist/bun/instruction-detect.d.ts +66 -0
  56. package/dist/bun/instruction-detect.d.ts.map +1 -0
  57. package/dist/bun/log.d.ts +9 -0
  58. package/dist/bun/log.d.ts.map +1 -1
  59. package/dist/bun/ltm.d.ts +139 -5
  60. package/dist/bun/ltm.d.ts.map +1 -1
  61. package/dist/bun/pattern-extract.d.ts +7 -0
  62. package/dist/bun/pattern-extract.d.ts.map +1 -1
  63. package/dist/bun/prompt.d.ts +1 -1
  64. package/dist/bun/prompt.d.ts.map +1 -1
  65. package/dist/bun/recall.d.ts.map +1 -1
  66. package/dist/bun/search.d.ts +5 -3
  67. package/dist/bun/search.d.ts.map +1 -1
  68. package/dist/bun/session-limiter.d.ts +26 -0
  69. package/dist/bun/session-limiter.d.ts.map +1 -0
  70. package/dist/bun/temporal.d.ts +2 -0
  71. package/dist/bun/temporal.d.ts.map +1 -1
  72. package/dist/bun/types.d.ts +1 -1
  73. package/dist/node/agents-file.d.ts +4 -0
  74. package/dist/node/agents-file.d.ts.map +1 -1
  75. package/dist/node/config.d.ts +2 -0
  76. package/dist/node/config.d.ts.map +1 -1
  77. package/dist/node/curator.d.ts +45 -0
  78. package/dist/node/curator.d.ts.map +1 -1
  79. package/dist/node/data-dir.d.ts +18 -0
  80. package/dist/node/data-dir.d.ts.map +1 -0
  81. package/dist/node/db.d.ts +85 -0
  82. package/dist/node/db.d.ts.map +1 -1
  83. package/dist/node/distillation.d.ts +2 -13
  84. package/dist/node/distillation.d.ts.map +1 -1
  85. package/dist/node/embedding-vendor.d.ts +22 -38
  86. package/dist/node/embedding-vendor.d.ts.map +1 -1
  87. package/dist/node/embedding-worker-types.d.ts +17 -12
  88. package/dist/node/embedding-worker-types.d.ts.map +1 -1
  89. package/dist/node/embedding-worker.d.ts +9 -2
  90. package/dist/node/embedding-worker.d.ts.map +1 -1
  91. package/dist/node/embedding-worker.js +38864 -33
  92. package/dist/node/embedding-worker.js.map +4 -4
  93. package/dist/node/embedding.d.ts +35 -23
  94. package/dist/node/embedding.d.ts.map +1 -1
  95. package/dist/node/gradient.d.ts +17 -1
  96. package/dist/node/gradient.d.ts.map +1 -1
  97. package/dist/node/import/detect.d.ts +14 -0
  98. package/dist/node/import/detect.d.ts.map +1 -0
  99. package/dist/node/import/extract.d.ts +43 -0
  100. package/dist/node/import/extract.d.ts.map +1 -0
  101. package/dist/node/import/history.d.ts +40 -0
  102. package/dist/node/import/history.d.ts.map +1 -0
  103. package/dist/node/import/index.d.ts +17 -0
  104. package/dist/node/import/index.d.ts.map +1 -0
  105. package/dist/node/import/providers/aider.d.ts +2 -0
  106. package/dist/node/import/providers/aider.d.ts.map +1 -0
  107. package/dist/node/import/providers/claude-code.d.ts +2 -0
  108. package/dist/node/import/providers/claude-code.d.ts.map +1 -0
  109. package/dist/node/import/providers/cline.d.ts +2 -0
  110. package/dist/node/import/providers/cline.d.ts.map +1 -0
  111. package/dist/node/import/providers/codex.d.ts +2 -0
  112. package/dist/node/import/providers/codex.d.ts.map +1 -0
  113. package/dist/node/import/providers/continue.d.ts +2 -0
  114. package/dist/node/import/providers/continue.d.ts.map +1 -0
  115. package/dist/node/import/providers/index.d.ts +19 -0
  116. package/dist/node/import/providers/index.d.ts.map +1 -0
  117. package/dist/node/import/providers/opencode.d.ts +2 -0
  118. package/dist/node/import/providers/opencode.d.ts.map +1 -0
  119. package/dist/node/import/providers/pi.d.ts +2 -0
  120. package/dist/node/import/providers/pi.d.ts.map +1 -0
  121. package/dist/node/import/types.d.ts +82 -0
  122. package/dist/node/import/types.d.ts.map +1 -0
  123. package/dist/node/index.d.ts +5 -2
  124. package/dist/node/index.d.ts.map +1 -1
  125. package/dist/node/index.js +3150 -439
  126. package/dist/node/index.js.map +4 -4
  127. package/dist/node/instruction-detect.d.ts +66 -0
  128. package/dist/node/instruction-detect.d.ts.map +1 -0
  129. package/dist/node/log.d.ts +9 -0
  130. package/dist/node/log.d.ts.map +1 -1
  131. package/dist/node/ltm.d.ts +139 -5
  132. package/dist/node/ltm.d.ts.map +1 -1
  133. package/dist/node/pattern-extract.d.ts +7 -0
  134. package/dist/node/pattern-extract.d.ts.map +1 -1
  135. package/dist/node/prompt.d.ts +1 -1
  136. package/dist/node/prompt.d.ts.map +1 -1
  137. package/dist/node/recall.d.ts.map +1 -1
  138. package/dist/node/search.d.ts +5 -3
  139. package/dist/node/search.d.ts.map +1 -1
  140. package/dist/node/session-limiter.d.ts +26 -0
  141. package/dist/node/session-limiter.d.ts.map +1 -0
  142. package/dist/node/temporal.d.ts +2 -0
  143. package/dist/node/temporal.d.ts.map +1 -1
  144. package/dist/node/types.d.ts +1 -1
  145. package/dist/types/agents-file.d.ts +4 -0
  146. package/dist/types/agents-file.d.ts.map +1 -1
  147. package/dist/types/config.d.ts +2 -0
  148. package/dist/types/config.d.ts.map +1 -1
  149. package/dist/types/curator.d.ts +45 -0
  150. package/dist/types/curator.d.ts.map +1 -1
  151. package/dist/types/data-dir.d.ts +18 -0
  152. package/dist/types/data-dir.d.ts.map +1 -0
  153. package/dist/types/db.d.ts +85 -0
  154. package/dist/types/db.d.ts.map +1 -1
  155. package/dist/types/distillation.d.ts +2 -13
  156. package/dist/types/distillation.d.ts.map +1 -1
  157. package/dist/types/embedding-vendor.d.ts +22 -38
  158. package/dist/types/embedding-vendor.d.ts.map +1 -1
  159. package/dist/types/embedding-worker-types.d.ts +17 -12
  160. package/dist/types/embedding-worker-types.d.ts.map +1 -1
  161. package/dist/types/embedding-worker.d.ts +9 -2
  162. package/dist/types/embedding-worker.d.ts.map +1 -1
  163. package/dist/types/embedding.d.ts +35 -23
  164. package/dist/types/embedding.d.ts.map +1 -1
  165. package/dist/types/gradient.d.ts +17 -1
  166. package/dist/types/gradient.d.ts.map +1 -1
  167. package/dist/types/import/detect.d.ts +14 -0
  168. package/dist/types/import/detect.d.ts.map +1 -0
  169. package/dist/types/import/extract.d.ts +43 -0
  170. package/dist/types/import/extract.d.ts.map +1 -0
  171. package/dist/types/import/history.d.ts +40 -0
  172. package/dist/types/import/history.d.ts.map +1 -0
  173. package/dist/types/import/index.d.ts +17 -0
  174. package/dist/types/import/index.d.ts.map +1 -0
  175. package/dist/types/import/providers/aider.d.ts +2 -0
  176. package/dist/types/import/providers/aider.d.ts.map +1 -0
  177. package/dist/types/import/providers/claude-code.d.ts +2 -0
  178. package/dist/types/import/providers/claude-code.d.ts.map +1 -0
  179. package/dist/types/import/providers/cline.d.ts +2 -0
  180. package/dist/types/import/providers/cline.d.ts.map +1 -0
  181. package/dist/types/import/providers/codex.d.ts +2 -0
  182. package/dist/types/import/providers/codex.d.ts.map +1 -0
  183. package/dist/types/import/providers/continue.d.ts +2 -0
  184. package/dist/types/import/providers/continue.d.ts.map +1 -0
  185. package/dist/types/import/providers/index.d.ts +19 -0
  186. package/dist/types/import/providers/index.d.ts.map +1 -0
  187. package/dist/types/import/providers/opencode.d.ts +2 -0
  188. package/dist/types/import/providers/opencode.d.ts.map +1 -0
  189. package/dist/types/import/providers/pi.d.ts +2 -0
  190. package/dist/types/import/providers/pi.d.ts.map +1 -0
  191. package/dist/types/import/types.d.ts +82 -0
  192. package/dist/types/import/types.d.ts.map +1 -0
  193. package/dist/types/index.d.ts +5 -2
  194. package/dist/types/index.d.ts.map +1 -1
  195. package/dist/types/instruction-detect.d.ts +66 -0
  196. package/dist/types/instruction-detect.d.ts.map +1 -0
  197. package/dist/types/log.d.ts +9 -0
  198. package/dist/types/log.d.ts.map +1 -1
  199. package/dist/types/ltm.d.ts +139 -5
  200. package/dist/types/ltm.d.ts.map +1 -1
  201. package/dist/types/pattern-extract.d.ts +7 -0
  202. package/dist/types/pattern-extract.d.ts.map +1 -1
  203. package/dist/types/prompt.d.ts +1 -1
  204. package/dist/types/prompt.d.ts.map +1 -1
  205. package/dist/types/recall.d.ts.map +1 -1
  206. package/dist/types/search.d.ts +5 -3
  207. package/dist/types/search.d.ts.map +1 -1
  208. package/dist/types/session-limiter.d.ts +26 -0
  209. package/dist/types/session-limiter.d.ts.map +1 -0
  210. package/dist/types/temporal.d.ts +2 -0
  211. package/dist/types/temporal.d.ts.map +1 -1
  212. package/dist/types/types.d.ts +1 -1
  213. package/package.json +3 -4
  214. package/src/agents-file.ts +41 -13
  215. package/src/config.ts +31 -18
  216. package/src/curator.ts +163 -75
  217. package/src/data-dir.ts +76 -0
  218. package/src/db.ts +457 -11
  219. package/src/distillation.ts +65 -16
  220. package/src/embedding-vendor.ts +23 -40
  221. package/src/embedding-worker-types.ts +19 -11
  222. package/src/embedding-worker.ts +111 -47
  223. package/src/embedding.ts +224 -174
  224. package/src/gradient.ts +192 -75
  225. package/src/import/detect.ts +37 -0
  226. package/src/import/extract.ts +137 -0
  227. package/src/import/history.ts +99 -0
  228. package/src/import/index.ts +45 -0
  229. package/src/import/providers/aider.ts +207 -0
  230. package/src/import/providers/claude-code.ts +339 -0
  231. package/src/import/providers/cline.ts +324 -0
  232. package/src/import/providers/codex.ts +369 -0
  233. package/src/import/providers/continue.ts +304 -0
  234. package/src/import/providers/index.ts +32 -0
  235. package/src/import/providers/opencode.ts +272 -0
  236. package/src/import/providers/pi.ts +332 -0
  237. package/src/import/types.ts +91 -0
  238. package/src/index.ts +13 -0
  239. package/src/instruction-detect.ts +275 -0
  240. package/src/log.ts +91 -3
  241. package/src/ltm.ts +789 -41
  242. package/src/pattern-extract.ts +41 -0
  243. package/src/prompt.ts +7 -1
  244. package/src/recall.ts +43 -5
  245. package/src/search.ts +7 -5
  246. package/src/session-limiter.ts +47 -0
  247. package/src/temporal.ts +18 -6
  248. package/src/types.ts +1 -1
@@ -12,6 +12,13 @@
12
12
  * - "prefers X for Y"
13
13
  * - "going with X because Y"
14
14
  *
15
+ * Also matches process instruction patterns from distilled observations
16
+ * where the observer normalizes user assertions:
17
+ * - "User stated always X"
18
+ * - "User said never Y"
19
+ * - "User stated make sure to X"
20
+ * - "User stated don't forget to X"
21
+ *
15
22
  * Extracted entries participate in the normal curator cycle — the curator
16
23
  * can consolidate or remove them based on actual value. The extraction is
17
24
  * a cheap seed, not a permanent fixture.
@@ -76,6 +83,33 @@ const PATTERNS: PatternDef[] = [
76
83
  category: "preference",
77
84
  titleFn: (m) => `Typically uses ${m[1].trim()}`,
78
85
  },
86
+
87
+ // Process instruction patterns — match distilled observations recording
88
+ // user assertions about workflow/process rules. The distillation observer
89
+ // normalizes user instructions into "User stated always X" phrasing.
90
+ // These require "stated/asserted/said" to avoid overlapping with the
91
+ // existing "typically uses" pattern above (which already handles
92
+ // "user always use/prefer/go with X").
93
+ {
94
+ regex: /(?:user |team |we )(?:stated |asserted |said )(?:to )?always (.+?)(?:\.|,|$)/gi,
95
+ category: "preference",
96
+ titleFn: (m) => `Always ${m[1].trim()}`,
97
+ },
98
+ {
99
+ regex: /(?:user |team |we )(?:stated |asserted |said )(?:to )?never (.+?)(?:\.|,|$)/gi,
100
+ category: "preference",
101
+ titleFn: (m) => `Never ${m[1].trim()}`,
102
+ },
103
+ {
104
+ regex: /(?:user |team |we )(?:stated |asserted |said )(?:to )?make sure to (.+?)(?:\.|,|$)/gi,
105
+ category: "preference",
106
+ titleFn: (m) => `Make sure to ${m[1].trim()}`,
107
+ },
108
+ {
109
+ regex: /(?:user |team |we )(?:stated |asserted |said )(?:to )?(?:don't|do not) forget (?:to )?(.+?)(?:\.|,|$)/gi,
110
+ category: "preference",
111
+ titleFn: (m) => `Always ${m[1].trim()}`,
112
+ },
79
113
  ];
80
114
 
81
115
  /**
@@ -96,6 +130,13 @@ export function extractPatterns(observations: string): ExtractedPattern[] {
96
130
  regex.lastIndex = 0;
97
131
  let match: RegExpMatchArray | null;
98
132
  while ((match = regex.exec(observations)) !== null) {
133
+ // Skip false positives: template placeholders (e.g. "X", "Y"),
134
+ // quoted fragments, or very short captures that are clearly not
135
+ // real technology/tool names. Plain apostrophes (') are allowed
136
+ // since they appear in valid names like "Bun's test runner".
137
+ const captures = match.slice(1);
138
+ if (captures.some((c) => c && (c.trim().length <= 2 || /["\u201C\u201D`\u2018\u2019]/.test(c)))) continue;
139
+
99
140
  const title = titleFn(match);
100
141
  const key = title.toLowerCase();
101
142
  if (seen.has(key)) continue;
package/src/prompt.ts CHANGED
@@ -222,6 +222,10 @@ Focus ONLY on knowledge that helps a coding agent work effectively on THIS codeb
222
222
  - Environment/tooling setup details that affect development
223
223
  - Important relationships between components that aren't obvious from reading the code
224
224
  - User preferences and working style specific to how they use this project
225
+ - Repeated user instructions — when the user says things like "always", "never",
226
+ "make sure to", "don't forget to", these are high-value preference candidates.
227
+ If you see instruction-like language, prioritize extracting it as a "preference" entry.
228
+ These instructions represent how the user wants to work and should persist across sessions.
225
229
 
226
230
  Do NOT extract:
227
231
  - Task-specific details (file currently being edited, current bug being fixed)
@@ -316,7 +320,9 @@ IMPORTANT:
316
320
  2. When updating, REPLACE the content with a complete rewrite — never append.
317
321
  3. If entries cover the same system from different angles, merge them: update one, delete the rest.
318
322
  4. Only create a new entry for genuinely distinct knowledge with no existing home.
319
- 5. Keep all entries under 150 words. If an existing entry is too long, use an update op to trim it.`;
323
+ 5. Keep all entries under 150 words. If an existing entry is too long, use an update op to trim it.
324
+ 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.`;
320
326
  }
321
327
 
322
328
  /**
package/src/recall.ts CHANGED
@@ -475,14 +475,27 @@ export async function searchRecall(
475
475
  }
476
476
  }
477
477
 
478
+ // Determine vector boost weight: for queries with enough meaningful terms,
479
+ // boost vector search lists so semantic similarity outweighs keyword noise.
480
+ const queryTermCount = filterTerms(query).length;
481
+ const vectorWeight =
482
+ queryTermCount >= (searchConfig?.vectorBoostMinTerms ?? 3)
483
+ ? (searchConfig?.vectorBoostWeight ?? 1.5)
484
+ : 1;
485
+
478
486
  // Collect per-query RRF lists. Original query is always first; if expansion
479
487
  // produced extras, we still weight the original twice by adding both original
480
488
  // and expanded lists (RRF naturally weights items appearing in more lists).
481
489
  const allRrfLists: Array<{
482
490
  items: TaggedResult[];
483
491
  key: (r: TaggedResult) => string;
492
+ weight?: number;
484
493
  }> = [];
485
494
 
495
+ // Track where primary (first-query) lists end so the MAX_RRF_LISTS cap
496
+ // trims expanded-query lists first, preserving vector/supplemental lists.
497
+ let primaryListEnd = 0;
498
+
486
499
  for (const q of queries) {
487
500
  const knowledgeResults: ltm.ScoredKnowledgeEntry[] = [];
488
501
  if (knowledgeEnabled && scope !== "session") {
@@ -568,7 +581,15 @@ export async function searchRecall(
568
581
  key: (r) => `t:${r.item.id}`,
569
582
  });
570
583
  }
584
+
585
+ // Mark the end of the first (original) query's lists. Supplemental lists
586
+ // (vector, lat.md, cross-project, quality, exact-match) are appended after
587
+ // the loop and should be preserved over expanded-query lists when capping.
588
+ if (primaryListEnd === 0) {
589
+ primaryListEnd = allRrfLists.length;
590
+ }
571
591
  }
592
+ const perQueryListEnd = allRrfLists.length;
572
593
 
573
594
  // Vector search on the original query (not expansions — avoid redundant embeds).
574
595
  if (embedding.isAvailable() && scope !== "session") {
@@ -593,6 +614,7 @@ export async function searchRecall(
593
614
  allRrfLists.push({
594
615
  items: vectorTagged,
595
616
  key: (r) => `k:${r.item.id}`,
617
+ weight: vectorWeight,
596
618
  });
597
619
  }
598
620
  }
@@ -618,6 +640,7 @@ export async function searchRecall(
618
640
  allRrfLists.push({
619
641
  items: distVectorTagged,
620
642
  key: (r) => `d:${r.item.id}`,
643
+ weight: vectorWeight,
621
644
  });
622
645
  }
623
646
  }
@@ -648,6 +671,7 @@ export async function searchRecall(
648
671
  allRrfLists.push({
649
672
  items: temporalVectorTagged,
650
673
  key: (r) => `t:${r.item.id}`,
674
+ weight: vectorWeight,
651
675
  });
652
676
  }
653
677
  }
@@ -786,6 +810,25 @@ export async function searchRecall(
786
810
  }
787
811
  }
788
812
 
813
+ // Cap the number of RRF lists to prevent score inflation from marginal items.
814
+ // With query expansion (3 queries × 4 sources + supplemental lists), the list
815
+ // count can exceed 15. Each list gives marginal items enough cumulative RRF
816
+ // score to clear the relevance floor.
817
+ //
818
+ // Priority: primary (original query BM25 + recency) and supplemental
819
+ // (vector, lat.md, cross-project, quality, exact-match) are high-value.
820
+ // Expanded-query BM25 lists are lowest priority — trim those first.
821
+ const MAX_RRF_LISTS = 10;
822
+ if (allRrfLists.length > MAX_RRF_LISTS) {
823
+ // Layout: [0..primaryListEnd) = primary, [primaryListEnd..perQueryEnd) = expanded, [perQueryEnd..) = supplemental
824
+ const primary = allRrfLists.slice(0, primaryListEnd);
825
+ const expanded = allRrfLists.slice(primaryListEnd, perQueryListEnd);
826
+ const supplemental = allRrfLists.slice(perQueryListEnd);
827
+ const budget = Math.max(0, MAX_RRF_LISTS - primary.length - supplemental.length);
828
+ allRrfLists.length = 0;
829
+ allRrfLists.push(...primary, ...expanded.slice(0, budget), ...supplemental);
830
+ }
831
+
789
832
  const fused = reciprocalRankFusion<TaggedResult>(allRrfLists);
790
833
 
791
834
  // Cap output: return at most 3x the per-source limit. With 7+ RRF sources
@@ -885,11 +928,6 @@ export async function runRecall(input: RecallInput): Promise<RecallResult> {
885
928
  return recallById(input.id);
886
929
  }
887
930
 
888
- // Short-circuit vague queries — stopwords-only would match everything.
889
- if (ftsQuery(input.query) === EMPTY_QUERY) {
890
- return "Query too vague — try using specific keywords, file names, or technical terms.";
891
- }
892
-
893
931
  const fused = await searchRecall(input);
894
932
  const recallCfg = input.searchConfig?.recall;
895
933
  return formatFusedResults(fused, {
package/src/search.ts CHANGED
@@ -302,29 +302,31 @@ export function normalizeRank(
302
302
  /**
303
303
  * Reciprocal Rank Fusion: merge multiple ranked lists into a single ranked list.
304
304
  *
305
- * RRF score = Σ(1 / (k + rank_i)) for each list where the item appears.
305
+ * RRF score = Σ(weight / (k + rank_i)) for each list where the item appears.
306
306
  * k = 60 is standard (from Cormack et al., 2009; also used by QMD).
307
307
  *
308
308
  * RRF is rank-based, not score-based — raw score magnitude differences across
309
309
  * different FTS5 tables don't matter. Only relative ordering within each list.
310
310
  *
311
- * @param lists Each list provides items (in ranked order) and a key function
312
- * for deduplication. Items at the front of the array are rank 0.
311
+ * @param lists Each list provides items (in ranked order), a key function
312
+ * for deduplication, and an optional weight (default 1).
313
+ * Items at the front of the array are rank 0.
313
314
  * @param k Smoothing constant. Default 60.
314
315
  * @returns Fused list sorted by RRF score descending. When items appear
315
316
  * in multiple lists, the first occurrence's item is kept.
316
317
  */
317
318
  export function reciprocalRankFusion<T>(
318
- lists: Array<{ items: T[]; key: (item: T) => string }>,
319
+ lists: Array<{ items: T[]; key: (item: T) => string; weight?: number }>,
319
320
  k = 60,
320
321
  ): Array<{ item: T; score: number }> {
321
322
  const scores = new Map<string, { item: T; score: number }>();
322
323
 
323
324
  for (const list of lists) {
325
+ const w = list.weight ?? 1;
324
326
  for (let rank = 0; rank < list.items.length; rank++) {
325
327
  const item = list.items[rank];
326
328
  const id = list.key(item);
327
- const rrfScore = 1 / (k + rank);
329
+ const rrfScore = w / (k + rank);
328
330
  const existing = scores.get(id);
329
331
  if (existing) {
330
332
  existing.score += rrfScore;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Per-key concurrency limiter using p-limit.
3
+ *
4
+ * Each key (typically a session ID) gets its own p-limit(1) instance,
5
+ * serializing async operations on the same key while allowing different
6
+ * keys to run fully in parallel.
7
+ *
8
+ * Two independent limiter pools are provided — one for distillation and
9
+ * one for curation — so they don't block each other.
10
+ */
11
+
12
+ import pLimit from "p-limit";
13
+
14
+ type LimitFunction = ReturnType<typeof pLimit>;
15
+
16
+ function createLimiterPool() {
17
+ const limiters = new Map<string, LimitFunction>();
18
+
19
+ /** Get or create a p-limit(1) limiter for the given key. */
20
+ function get(key: string): LimitFunction {
21
+ let limiter = limiters.get(key);
22
+ if (!limiter) {
23
+ limiter = pLimit(1);
24
+ limiters.set(key, limiter);
25
+ }
26
+ return limiter;
27
+ }
28
+
29
+ /** Check if a limiter for `key` is currently busy (active or pending work). */
30
+ function isBusy(key: string): boolean {
31
+ const limiter = limiters.get(key);
32
+ return limiter ? limiter.activeCount + limiter.pendingCount > 0 : false;
33
+ }
34
+
35
+ /** Clear all limiters (for test cleanup). */
36
+ function clear(): void {
37
+ limiters.clear();
38
+ }
39
+
40
+ return { get, isBusy, clear };
41
+ }
42
+
43
+ /** Serializes distillation.run() and metaDistill() per session. */
44
+ export const distillLimiter = createLimiterPool();
45
+
46
+ /** Serializes curator.run() per session with skip-if-busy semantics. */
47
+ export const curatorLimiter = createLimiterPool();
package/src/temporal.ts CHANGED
@@ -171,6 +171,8 @@ export function markDistilled(ids: string[]) {
171
171
  .run(...ids);
172
172
  }
173
173
 
174
+ // Only searches undistilled messages — distilled content is already represented
175
+ // in distillation search results and would duplicate/dilute temporal hits.
174
176
  // LIKE-based fallback for when FTS5 fails unexpectedly.
175
177
  function searchLike(input: {
176
178
  pid: string;
@@ -186,8 +188,8 @@ function searchLike(input: {
186
188
  const conditions = terms.map(() => "LOWER(content) LIKE ?").join(" AND ");
187
189
  const likeParams = terms.map((t) => `%${t}%`);
188
190
  const query = input.sessionID
189
- ? `SELECT * FROM temporal_messages WHERE project_id = ? AND session_id = ? AND ${conditions} ORDER BY created_at DESC LIMIT ?`
190
- : `SELECT * FROM temporal_messages WHERE project_id = ? AND ${conditions} ORDER BY created_at DESC LIMIT ?`;
191
+ ? `SELECT * FROM temporal_messages WHERE project_id = ? AND session_id = ? AND distilled = 0 AND ${conditions} ORDER BY created_at DESC LIMIT ?`
192
+ : `SELECT * FROM temporal_messages WHERE project_id = ? AND distilled = 0 AND ${conditions} ORDER BY created_at DESC LIMIT ?`;
191
193
  const params = input.sessionID
192
194
  ? [input.pid, input.sessionID, ...likeParams, input.limit]
193
195
  : [input.pid, ...likeParams, input.limit];
@@ -208,11 +210,11 @@ export function search(input: {
208
210
  const ftsSQL = input.sessionID
209
211
  ? `SELECT m.* FROM temporal_fts f
210
212
  CROSS JOIN temporal_messages m ON m.rowid = f.rowid
211
- WHERE f.content MATCH ? AND m.project_id = ? AND m.session_id = ?
213
+ WHERE f.content MATCH ? AND m.project_id = ? AND m.session_id = ? AND m.distilled = 0
212
214
  ORDER BY rank LIMIT ?`
213
215
  : `SELECT m.* FROM temporal_fts f
214
216
  CROSS JOIN temporal_messages m ON m.rowid = f.rowid
215
- WHERE f.content MATCH ? AND m.project_id = ?
217
+ WHERE f.content MATCH ? AND m.project_id = ? AND m.distilled = 0
216
218
  ORDER BY rank LIMIT ?`;
217
219
 
218
220
  try {
@@ -251,11 +253,11 @@ export function searchScored(input: {
251
253
  const ftsSQL = input.sessionID
252
254
  ? `SELECT m.*, rank FROM temporal_fts f
253
255
  CROSS JOIN temporal_messages m ON m.rowid = f.rowid
254
- WHERE f.content MATCH ? AND m.project_id = ? AND m.session_id = ?
256
+ WHERE f.content MATCH ? AND m.project_id = ? AND m.session_id = ? AND m.distilled = 0
255
257
  ORDER BY rank LIMIT ?`
256
258
  : `SELECT m.*, rank FROM temporal_fts f
257
259
  CROSS JOIN temporal_messages m ON m.rowid = f.rowid
258
- WHERE f.content MATCH ? AND m.project_id = ?
260
+ WHERE f.content MATCH ? AND m.project_id = ? AND m.distilled = 0
259
261
  ORDER BY rank LIMIT ?`;
260
262
 
261
263
  try {
@@ -322,6 +324,16 @@ export function count(projectPath: string, sessionID?: string): number {
322
324
  ).count;
323
325
  }
324
326
 
327
+ /** Quick existence check — true if any temporal messages exist for this session. */
328
+ export function hasMessages(projectPath: string, sessionID: string): boolean {
329
+ const pid = ensureProject(projectPath);
330
+ return !!db()
331
+ .query(
332
+ "SELECT 1 FROM temporal_messages WHERE project_id = ? AND session_id = ? LIMIT 1",
333
+ )
334
+ .get(pid, sessionID);
335
+ }
336
+
325
337
  export function undistilledCount(
326
338
  projectPath: string,
327
339
  sessionID?: string,
package/src/types.ts CHANGED
@@ -183,7 +183,7 @@ export type LoreMessageWithParts = {
183
183
  * Host adapters implement this:
184
184
  * - OpenCode: wraps `client.session.create()` + `client.session.prompt()`
185
185
  * - Pi: wraps `complete()` from `@mariozechner/pi-ai`
186
- * - Standalone: direct `fetch()` to provider APIs
186
+ * - Gateway: direct `fetch()` to provider APIs
187
187
  */
188
188
  export interface LLMClient {
189
189
  /**