recallnest 1.4.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.
Files changed (261) hide show
  1. package/.claude-plugin/CLAUDE.md +29 -0
  2. package/.claude-plugin/marketplace.json +18 -0
  3. package/.env.example +3 -0
  4. package/.github/workflows/ci.yml +28 -0
  5. package/.mcp.json +9 -0
  6. package/CHANGELOG.md +51 -0
  7. package/CLAUDE.md +42 -0
  8. package/DEMO.md +94 -0
  9. package/EVAL.md +89 -0
  10. package/FAILURES.md +75 -0
  11. package/LICENSE +21 -0
  12. package/OPERATIONS.md +59 -0
  13. package/README.md +427 -0
  14. package/README_CN.md +425 -0
  15. package/ROADMAP.md +198 -0
  16. package/assets/recallnest-workbench.png +0 -0
  17. package/assets/ui/app.js +807 -0
  18. package/assets/ui/index.html +154 -0
  19. package/assets/ui/styles.css +605 -0
  20. package/assets/wechat-qr.jpg +0 -0
  21. package/config.json.example +38 -0
  22. package/daily-recall.sh +87 -0
  23. package/daily-reflections/2026-03-05.md +15 -0
  24. package/daily-reflections/2026-03-06.md +15 -0
  25. package/daily-reflections/2026-03-07.md +7 -0
  26. package/daily-reflections/2026-03-08.md +34 -0
  27. package/daily-reflections/2026-03-09.md +35 -0
  28. package/daily-reflections/2026-03-10.md +36 -0
  29. package/daily-reflections/2026-03-11.md +35 -0
  30. package/daily-reflections/2026-03-12.md +36 -0
  31. package/daily-reflections/2026-03-13.md +35 -0
  32. package/daily-reflections/2026-03-14.md +35 -0
  33. package/daily-reflections/2026-03-15.md +35 -0
  34. package/daily-reflections/2026-03-16.md +41 -0
  35. package/daily-reflections/2026-03-17.md +10 -0
  36. package/daily-reflections/2026-03-18.md +10 -0
  37. package/daily-reflections/2026-03-19.md +10 -0
  38. package/daily-reflections/2026-03-20.md +10 -0
  39. package/daily-reflections/2026-03-21.md +10 -0
  40. package/daily-reflections/2026-03-22.md +10 -0
  41. package/daily-reflections/2026-03-23.md +10 -0
  42. package/daily-reflections/2026-03-24.md +10 -0
  43. package/daily-reflections/2026-03-25.md +32 -0
  44. package/daily-reflections/2026-03-26.md +37 -0
  45. package/daily-reflections/2026-03-27.md +35 -0
  46. package/daily-reflections/2026-03-28.md +35 -0
  47. package/daily-reflections/2026-03-29.md +35 -0
  48. package/daily-reflections/2026-03-30.md +36 -0
  49. package/daily-reflections/2026-03-31.md +35 -0
  50. package/daily-reflections/2026-04-01.md +35 -0
  51. package/docs/api-reference.md +1291 -0
  52. package/docs/architecture.md +168 -0
  53. package/docs/memory-boundary-contract.md +226 -0
  54. package/docs/memory-categories.md +119 -0
  55. package/docs/pipeline-audit-ultramemory.md +177 -0
  56. package/docs/plans/2026-03-08-query-expansion.md +270 -0
  57. package/docs/plans/2026-03-16-continuity-layer.md +196 -0
  58. package/docs/plans/mcp-tiering-plan.md +126 -0
  59. package/docs/research/recall-quality-audit-2026-03-22.md +153 -0
  60. package/docs/self-evolution.md +198 -0
  61. package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +782 -0
  62. package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +77 -0
  63. package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +95 -0
  64. package/eval/cases.json +85 -0
  65. package/eval/continuity/case-seeds.json +90 -0
  66. package/eval/continuity/cases.json +302 -0
  67. package/eval/continuity/memory-seeds.json +26 -0
  68. package/eval/continuity/pattern-seeds.json +100 -0
  69. package/eval/continuity/results-2026-03-16.md +117 -0
  70. package/eval/continuity/results-2026-03-19.md +453 -0
  71. package/eval/continuity/runbook.md +110 -0
  72. package/eval/continuity/seed-cases.md +54 -0
  73. package/eval/reports/2026-03-06-abstract-query-baseline.md +96 -0
  74. package/eval/reports/2026-03-06-aws-ssh-baseline.md +82 -0
  75. package/eval/reports/2026-03-06-baseline.md +82 -0
  76. package/eval/reports/query-expansion.md +138 -0
  77. package/integrations/README.md +57 -0
  78. package/integrations/claude-code/README.md +133 -0
  79. package/integrations/claude-code/claude-md-snippet.md +31 -0
  80. package/integrations/claude-code/setup.sh +66 -0
  81. package/integrations/claude-code/smoke-continuity.sh +452 -0
  82. package/integrations/codex/README.md +49 -0
  83. package/integrations/codex/agents-md-snippet.md +31 -0
  84. package/integrations/codex/setup.sh +58 -0
  85. package/integrations/examples/claude-agent-sdk/memory-agent.ts +182 -0
  86. package/integrations/examples/langchain/memory-chain.py +111 -0
  87. package/integrations/examples/openai-agents-sdk/memory-agent.py +109 -0
  88. package/integrations/gemini-cli/README.md +57 -0
  89. package/integrations/gemini-cli/gemini-md-snippet.md +31 -0
  90. package/integrations/gemini-cli/setup.sh +64 -0
  91. package/integrations/shared/install-managed-block.sh +51 -0
  92. package/lm +19 -0
  93. package/package.json +50 -0
  94. package/plan-v1.2-distributable.md +105 -0
  95. package/scripts/archive-distilled-junk.ts +116 -0
  96. package/scripts/backfill-l0l1l2.ts +98 -0
  97. package/scripts/blind-scan.ts +588 -0
  98. package/scripts/cleanup-step1-exact-dups.ts +117 -0
  99. package/scripts/cleanup-step2-template-dups.ts +113 -0
  100. package/scripts/cleanup-step3-stale-content.ts +149 -0
  101. package/scripts/cleanup-step4-orphan-scopes.ts +131 -0
  102. package/scripts/delete-distilled-junk.ts +88 -0
  103. package/scripts/distill-facts.ts +305 -0
  104. package/scripts/health-check.ts +343 -0
  105. package/scripts/incremental-ingest.sh +71 -0
  106. package/scripts/mlp-health-check.ts +382 -0
  107. package/scripts/pull-claude-desktop.ts +515 -0
  108. package/scripts/re-ingest-cc.sh +66 -0
  109. package/scripts/rebalance-memory-health.ts +181 -0
  110. package/scripts/run-desktop-ingest.sh +11 -0
  111. package/scripts/start-server.sh +12 -0
  112. package/scripts/viz-dashboard.html +423 -0
  113. package/scripts/viz-export.ts +703 -0
  114. package/src/__tests__/access-tracker-novelty.test.ts +228 -0
  115. package/src/__tests__/auto-consolidation.test.ts +168 -0
  116. package/src/__tests__/auto-recall.test.ts +131 -0
  117. package/src/__tests__/batch-dedup.test.ts +65 -0
  118. package/src/__tests__/capture-engine.test.ts +1142 -0
  119. package/src/__tests__/capture-heuristic.test.ts +171 -0
  120. package/src/__tests__/cascade-forget.test.ts +210 -0
  121. package/src/__tests__/category-min-scores.test.ts +33 -0
  122. package/src/__tests__/confidence-tracker.test.ts +196 -0
  123. package/src/__tests__/conflict-advisor.test.ts +206 -0
  124. package/src/__tests__/conflict-escalation.test.ts +149 -0
  125. package/src/__tests__/conflict-lifecycle.test.ts +83 -0
  126. package/src/__tests__/conflict-output.test.ts +85 -0
  127. package/src/__tests__/conflict-store.test.ts +374 -0
  128. package/src/__tests__/consolidation-engine.test.ts +181 -0
  129. package/src/__tests__/context-composer-pins.test.ts +64 -0
  130. package/src/__tests__/context-composer-task-selection.test.ts +24 -0
  131. package/src/__tests__/context-renderer.test.ts +91 -0
  132. package/src/__tests__/continuity-eval.test.ts +275 -0
  133. package/src/__tests__/continuity-schema.test.ts +97 -0
  134. package/src/__tests__/core-recall-eval.test.ts +235 -0
  135. package/src/__tests__/core-summary.test.ts +109 -0
  136. package/src/__tests__/dedup-actions.test.ts +58 -0
  137. package/src/__tests__/deterministic-id.test.ts +39 -0
  138. package/src/__tests__/doctor.test.ts +303 -0
  139. package/src/__tests__/drill-down.test.ts +108 -0
  140. package/src/__tests__/embedder.test.ts +122 -0
  141. package/src/__tests__/entity-resolver.test.ts +58 -0
  142. package/src/__tests__/event-segmenter.test.ts +153 -0
  143. package/src/__tests__/hotness-blend.test.ts +76 -0
  144. package/src/__tests__/ingest.test.ts +360 -0
  145. package/src/__tests__/kg-extractor.test.ts +184 -0
  146. package/src/__tests__/kg-store.test.ts +198 -0
  147. package/src/__tests__/llm-consolidation.test.ts +226 -0
  148. package/src/__tests__/memory-boundaries.test.ts +237 -0
  149. package/src/__tests__/memory-health-rebalance.test.ts +144 -0
  150. package/src/__tests__/memory-output.test.ts +215 -0
  151. package/src/__tests__/multi-vector.test.ts +242 -0
  152. package/src/__tests__/ppr-traversal.test.ts +256 -0
  153. package/src/__tests__/preference-guard-smoke.test.ts +211 -0
  154. package/src/__tests__/preference-matcher.test.ts +230 -0
  155. package/src/__tests__/preference-slots.test.ts +106 -0
  156. package/src/__tests__/prospective-memory.test.ts +283 -0
  157. package/src/__tests__/query-entity-detector.test.ts +171 -0
  158. package/src/__tests__/query-expander.test.ts +34 -0
  159. package/src/__tests__/recall-mode.test.ts +64 -0
  160. package/src/__tests__/result-synthesizer.test.ts +198 -0
  161. package/src/__tests__/resume-context.test.ts +4743 -0
  162. package/src/__tests__/retrieval-trace.test.ts +213 -0
  163. package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
  164. package/src/__tests__/retroactive-boost.test.ts +223 -0
  165. package/src/__tests__/scope-inventory.test.ts +200 -0
  166. package/src/__tests__/scope-policy.test.ts +69 -0
  167. package/src/__tests__/session-checkpoint.test.ts +259 -0
  168. package/src/__tests__/session-gc.test.ts +215 -0
  169. package/src/__tests__/store-scope-audit.test.ts +81 -0
  170. package/src/__tests__/temporal-parser.test.ts +241 -0
  171. package/src/__tests__/term-registry.test.ts +121 -0
  172. package/src/__tests__/tool-output-compressor.test.ts +133 -0
  173. package/src/__tests__/version-manager.test.ts +213 -0
  174. package/src/__tests__/workflow-observation.test.ts +400 -0
  175. package/src/access-tracker.ts +316 -0
  176. package/src/adaptive-retrieval.ts +64 -0
  177. package/src/api-server.ts +667 -0
  178. package/src/asset-sync.ts +79 -0
  179. package/src/auto-consolidation.ts +133 -0
  180. package/src/auto-gc.ts +124 -0
  181. package/src/auto-recall.ts +95 -0
  182. package/src/capture-engine.ts +931 -0
  183. package/src/capture-heuristic.ts +177 -0
  184. package/src/cascade-forget.ts +135 -0
  185. package/src/chunker.ts +253 -0
  186. package/src/cli.ts +1917 -0
  187. package/src/confidence-tracker.ts +166 -0
  188. package/src/conflict-advisor.ts +433 -0
  189. package/src/conflict-engine.ts +181 -0
  190. package/src/conflict-escalation.ts +138 -0
  191. package/src/conflict-lifecycle.ts +67 -0
  192. package/src/conflict-output.ts +218 -0
  193. package/src/conflict-schema.ts +130 -0
  194. package/src/conflict-store.ts +127 -0
  195. package/src/consolidation-engine.ts +256 -0
  196. package/src/context-composer-pins.ts +183 -0
  197. package/src/context-composer-queries.ts +121 -0
  198. package/src/context-composer-scope.ts +56 -0
  199. package/src/context-composer-stable-selection.ts +200 -0
  200. package/src/context-composer-stable.ts +217 -0
  201. package/src/context-composer-task-fallbacks.ts +36 -0
  202. package/src/context-composer-task-ranking.ts +197 -0
  203. package/src/context-composer-task-results.ts +137 -0
  204. package/src/context-composer-task-selection.ts +342 -0
  205. package/src/context-composer-text.ts +57 -0
  206. package/src/context-composer.ts +300 -0
  207. package/src/context-renderer.ts +179 -0
  208. package/src/decay-engine.ts +189 -0
  209. package/src/doctor.ts +543 -0
  210. package/src/embedder.ts +557 -0
  211. package/src/entity-resolver.ts +131 -0
  212. package/src/eval.ts +632 -0
  213. package/src/event-segmenter.ts +216 -0
  214. package/src/ingest.ts +1822 -0
  215. package/src/kg-extractor.ts +217 -0
  216. package/src/kg-store.ts +353 -0
  217. package/src/legacy-scope-review.ts +53 -0
  218. package/src/llm-client.ts +629 -0
  219. package/src/llm-consolidation.ts +168 -0
  220. package/src/mcp-server.ts +1337 -0
  221. package/src/memory-assets.ts +475 -0
  222. package/src/memory-boundaries.ts +354 -0
  223. package/src/memory-health-rebalance.ts +220 -0
  224. package/src/memory-output.ts +474 -0
  225. package/src/memory-schema.ts +184 -0
  226. package/src/multi-vector.ts +234 -0
  227. package/src/noise-filter.ts +155 -0
  228. package/src/ppr-traversal.ts +279 -0
  229. package/src/preference-matcher.ts +174 -0
  230. package/src/preference-slots.ts +259 -0
  231. package/src/prospective-memory.ts +198 -0
  232. package/src/query-entity-detector.ts +207 -0
  233. package/src/query-expander.ts +98 -0
  234. package/src/result-synthesizer.ts +114 -0
  235. package/src/retrieval-profiles.ts +101 -0
  236. package/src/retrieval-trace.ts +94 -0
  237. package/src/retriever.ts +1338 -0
  238. package/src/retroactive-boost.ts +154 -0
  239. package/src/rif.ts +73 -0
  240. package/src/runtime-config.ts +172 -0
  241. package/src/schema-utils.ts +56 -0
  242. package/src/scope-inventory.ts +310 -0
  243. package/src/scope-policy.ts +142 -0
  244. package/src/session-engine.ts +168 -0
  245. package/src/session-output.ts +96 -0
  246. package/src/session-schema.ts +60 -0
  247. package/src/session-store.ts +293 -0
  248. package/src/stderr-log.ts +16 -0
  249. package/src/store.ts +903 -0
  250. package/src/temporal-parser.ts +318 -0
  251. package/src/term-registry.ts +791 -0
  252. package/src/tool-output-compressor.ts +115 -0
  253. package/src/tracker.ts +57 -0
  254. package/src/ui-server.ts +430 -0
  255. package/src/version-manager.ts +172 -0
  256. package/src/workflow-observation-engine.ts +316 -0
  257. package/src/workflow-observation-managed.ts +83 -0
  258. package/src/workflow-observation-output.ts +86 -0
  259. package/src/workflow-observation-schema.ts +87 -0
  260. package/src/workflow-observation-scope-review.ts +409 -0
  261. package/src/workflow-observation-store.ts +69 -0
@@ -0,0 +1,557 @@
1
+ /**
2
+ * Embedding Abstraction Layer
3
+ * OpenAI-compatible API for various embedding providers.
4
+ * Supports automatic chunking for documents exceeding embedding context limits.
5
+ *
6
+ * Note: Some providers (e.g. Jina) support extra parameters like `task` and
7
+ * `normalized` on the embeddings endpoint. The OpenAI SDK types do not include
8
+ * these fields, so we pass them via a narrow `any` cast.
9
+ */
10
+
11
+ import OpenAI from "openai";
12
+ import { createHash } from "node:crypto";
13
+ import { smartChunk } from "./chunker.js";
14
+ import { logInfo, logWarn } from "./stderr-log.js";
15
+
16
+ const EMBEDDING_RETRY_DELAYS_MS = [200, 500];
17
+ const EMBEDDING_MAX_CONCURRENCY = 4;
18
+
19
+ // ============================================================================
20
+ // Embedding Cache (LRU with TTL)
21
+ // ============================================================================
22
+
23
+ interface CacheEntry {
24
+ vector: number[];
25
+ createdAt: number;
26
+ }
27
+
28
+ class EmbeddingCache {
29
+ private cache = new Map<string, CacheEntry>();
30
+ private readonly maxSize: number;
31
+ private readonly ttlMs: number;
32
+ public hits = 0;
33
+ public misses = 0;
34
+
35
+ constructor(maxSize = 256, ttlMinutes = 30) {
36
+ this.maxSize = maxSize;
37
+ this.ttlMs = ttlMinutes * 60_000;
38
+ }
39
+
40
+ private key(text: string, task?: string): string {
41
+ const hash = createHash("sha256").update(`${task || ""}:${text}`).digest("hex").slice(0, 24);
42
+ return hash;
43
+ }
44
+
45
+ get(text: string, task?: string): number[] | undefined {
46
+ const k = this.key(text, task);
47
+ const entry = this.cache.get(k);
48
+ if (!entry) {
49
+ this.misses++;
50
+ return undefined;
51
+ }
52
+ if (Date.now() - entry.createdAt > this.ttlMs) {
53
+ this.cache.delete(k);
54
+ this.misses++;
55
+ return undefined;
56
+ }
57
+ // Move to end (most recently used)
58
+ this.cache.delete(k);
59
+ this.cache.set(k, entry);
60
+ this.hits++;
61
+ return entry.vector;
62
+ }
63
+
64
+ set(text: string, task: string | undefined, vector: number[]): void {
65
+ const k = this.key(text, task);
66
+ // Evict oldest if full
67
+ if (this.cache.size >= this.maxSize) {
68
+ const firstKey = this.cache.keys().next().value;
69
+ if (firstKey !== undefined) this.cache.delete(firstKey);
70
+ }
71
+ this.cache.set(k, { vector, createdAt: Date.now() });
72
+ }
73
+
74
+ get size(): number { return this.cache.size; }
75
+ get stats(): { size: number; hits: number; misses: number; hitRate: string } {
76
+ const total = this.hits + this.misses;
77
+ return {
78
+ size: this.cache.size,
79
+ hits: this.hits,
80
+ misses: this.misses,
81
+ hitRate: total > 0 ? `${((this.hits / total) * 100).toFixed(1)}%` : "N/A",
82
+ };
83
+ }
84
+ }
85
+
86
+ // ============================================================================
87
+ // Types & Configuration
88
+ // ============================================================================
89
+
90
+ export interface EmbeddingConfig {
91
+ provider: "openai-compatible";
92
+ /** API key. Optional for local providers (Ollama, Docker Model Runner). */
93
+ apiKey?: string;
94
+ model: string;
95
+ baseURL?: string;
96
+ dimensions?: number;
97
+
98
+ /** Optional task type for query embeddings (e.g. "retrieval.query") */
99
+ taskQuery?: string;
100
+ /** Optional task type for passage/document embeddings (e.g. "retrieval.passage") */
101
+ taskPassage?: string;
102
+ /** Optional flag to request normalized embeddings (provider-dependent, e.g. Jina v5) */
103
+ normalized?: boolean;
104
+ /** Enable automatic chunking for documents exceeding context limits (default: true) */
105
+ chunking?: boolean;
106
+ }
107
+
108
+ // Known embedding model dimensions
109
+ const EMBEDDING_DIMENSIONS: Record<string, number> = {
110
+ "text-embedding-3-small": 1536,
111
+ "text-embedding-3-large": 3072,
112
+ "text-embedding-004": 768,
113
+ "gemini-embedding-001": 3072,
114
+ "nomic-embed-text": 768,
115
+ "mxbai-embed-large": 1024,
116
+ "BAAI/bge-m3": 1024,
117
+ "all-MiniLM-L6-v2": 384,
118
+ "all-mpnet-base-v2": 512,
119
+
120
+ // Jina v5
121
+ "jina-embeddings-v5-text-small": 1024,
122
+ "jina-embeddings-v5-text-nano": 768,
123
+
124
+ // Qwen3 Embedding (Docker Model Runner / local)
125
+ "ai/qwen3-embedding": 1024,
126
+ "ai/qwen3-embedding:0.6B-F16": 1024,
127
+ "ai/qwen3-embedding:4B": 1024,
128
+ "ai/qwen3-embedding:4B-Q4_K_M": 1024,
129
+ "ai/qwen3-embedding:8B-Q4_K_M": 1024,
130
+ };
131
+
132
+ // ============================================================================
133
+ // Utility Functions
134
+ // ============================================================================
135
+
136
+ function resolveEnvVars(value: string): string {
137
+ return value.replace(/\$\{([^}]+)\}/g, (_, envVar) => {
138
+ const envValue = process.env[envVar];
139
+ if (!envValue) {
140
+ throw new Error(`Environment variable ${envVar} is not set`);
141
+ }
142
+ return envValue;
143
+ });
144
+ }
145
+
146
+ function sleep(ms: number): Promise<void> {
147
+ return new Promise((resolve) => setTimeout(resolve, ms));
148
+ }
149
+
150
+ function isTransientEmbeddingError(error: unknown): boolean {
151
+ if (error instanceof OpenAI.APIConnectionError || error instanceof OpenAI.APIConnectionTimeoutError) {
152
+ return true;
153
+ }
154
+
155
+ const message = error instanceof Error ? error.message : String(error);
156
+ return /ECONNRESET|socket connection was closed unexpectedly|connection error|timed out|network/i.test(message);
157
+ }
158
+
159
+ export function getVectorDimensions(model: string, overrideDims?: number): number {
160
+ if (overrideDims && overrideDims > 0) {
161
+ return overrideDims;
162
+ }
163
+
164
+ const dims = EMBEDDING_DIMENSIONS[model];
165
+ if (!dims) {
166
+ throw new Error(
167
+ `Unsupported embedding model: ${model}. Either add it to EMBEDDING_DIMENSIONS or set embedding.dimensions in config.`
168
+ );
169
+ }
170
+
171
+ return dims;
172
+ }
173
+
174
+ // ============================================================================
175
+ // Embedder Class
176
+ // ============================================================================
177
+
178
+ export class Embedder {
179
+ private client: OpenAI;
180
+ public readonly dimensions: number;
181
+ private readonly _cache: EmbeddingCache;
182
+ private readonly _maxConcurrentRequests = EMBEDDING_MAX_CONCURRENCY;
183
+ private _activeRequests = 0;
184
+ private readonly _pendingRequests: Array<() => void> = [];
185
+
186
+ private readonly _model: string;
187
+ private readonly _taskQuery?: string;
188
+ private readonly _taskPassage?: string;
189
+ private readonly _normalized?: boolean;
190
+
191
+ /** Optional requested dimensions to pass through to the embedding provider (OpenAI-compatible). */
192
+ private readonly _requestDimensions?: number;
193
+ /** Enable automatic chunking for long documents (default: true) */
194
+ private readonly _autoChunk: boolean;
195
+
196
+ constructor(config: EmbeddingConfig & { chunking?: boolean }) {
197
+ // Resolve environment variables in API key (fallback for local providers)
198
+ const resolvedApiKey = config.apiKey ? resolveEnvVars(config.apiKey) : "no-key-required";
199
+
200
+ this._model = config.model;
201
+ this._taskQuery = config.taskQuery;
202
+ this._taskPassage = config.taskPassage;
203
+ this._normalized = config.normalized;
204
+ this._requestDimensions = config.dimensions;
205
+ // Enable auto-chunking by default for better handling of long documents
206
+ this._autoChunk = config.chunking !== false;
207
+
208
+ this.client = new OpenAI({
209
+ apiKey: resolvedApiKey,
210
+ ...(config.baseURL ? { baseURL: config.baseURL } : {}),
211
+ });
212
+
213
+ this.dimensions = getVectorDimensions(config.model, config.dimensions);
214
+ this._cache = new EmbeddingCache(256, 30); // 256 entries, 30 min TTL
215
+ }
216
+
217
+ // --------------------------------------------------------------------------
218
+ // Backward-compatible API
219
+ // --------------------------------------------------------------------------
220
+
221
+ /**
222
+ * Backward-compatible embedding API.
223
+ *
224
+ * Historically the plugin used a single `embed()` method for both query and
225
+ * passage embeddings. With task-aware providers we treat this as passage.
226
+ */
227
+ async embed(text: string): Promise<number[]> {
228
+ return this.embedPassage(text);
229
+ }
230
+
231
+ /** Backward-compatible batch embedding API (treated as passage). */
232
+ async embedBatch(texts: string[]): Promise<number[][]> {
233
+ return this.embedBatchPassage(texts);
234
+ }
235
+
236
+ // --------------------------------------------------------------------------
237
+ // Task-aware API
238
+ // --------------------------------------------------------------------------
239
+
240
+ async embedQuery(text: string): Promise<number[]> {
241
+ return this.embedSingle(text, this._taskQuery);
242
+ }
243
+
244
+ async embedPassage(text: string): Promise<number[]> {
245
+ return this.embedSingle(text, this._taskPassage);
246
+ }
247
+
248
+ async embedBatchQuery(texts: string[]): Promise<number[][]> {
249
+ return this.embedMany(texts, this._taskQuery);
250
+ }
251
+
252
+ async embedBatchPassage(texts: string[]): Promise<number[][]> {
253
+ return this.embedMany(texts, this._taskPassage);
254
+ }
255
+
256
+ // --------------------------------------------------------------------------
257
+ // Internals
258
+ // --------------------------------------------------------------------------
259
+
260
+ private validateEmbedding(embedding: number[]): void {
261
+ if (!Array.isArray(embedding)) {
262
+ throw new Error(`Embedding is not an array (got ${typeof embedding})`);
263
+ }
264
+ if (embedding.length !== this.dimensions) {
265
+ throw new Error(
266
+ `Embedding dimension mismatch: expected ${this.dimensions}, got ${embedding.length}`
267
+ );
268
+ }
269
+ }
270
+
271
+ private buildPayload(input: string | string[], task?: string): any {
272
+ const payload: any = {
273
+ model: this.model,
274
+ input,
275
+ // Force float output to avoid SDK default base64 decoding path.
276
+ encoding_format: "float",
277
+ };
278
+
279
+ if (task) payload.task = task;
280
+ if (this._normalized !== undefined) payload.normalized = this._normalized;
281
+
282
+ // Some OpenAI-compatible providers support requesting a specific vector size.
283
+ // We only pass it through when explicitly configured to avoid breaking providers
284
+ // that reject unknown fields.
285
+ if (this._requestDimensions && this._requestDimensions > 0) {
286
+ payload.dimensions = this._requestDimensions;
287
+ }
288
+
289
+ return payload;
290
+ }
291
+
292
+ private async withEmbeddingSlot<T>(fn: () => Promise<T>): Promise<T> {
293
+ if (this._activeRequests >= this._maxConcurrentRequests) {
294
+ await new Promise<void>((resolve) => {
295
+ this._pendingRequests.push(resolve);
296
+ });
297
+ }
298
+
299
+ this._activeRequests += 1;
300
+ try {
301
+ return await fn();
302
+ } finally {
303
+ this._activeRequests = Math.max(0, this._activeRequests - 1);
304
+ const next = this._pendingRequests.shift();
305
+ next?.();
306
+ }
307
+ }
308
+
309
+ private async createEmbedding(input: string | string[], task?: string) {
310
+ let attempt = 0;
311
+
312
+ while (true) {
313
+ try {
314
+ return await this.withEmbeddingSlot(() =>
315
+ this.client.embeddings.create(this.buildPayload(input, task) as any)
316
+ );
317
+ } catch (error) {
318
+ if (!isTransientEmbeddingError(error) || attempt >= EMBEDDING_RETRY_DELAYS_MS.length) {
319
+ throw error;
320
+ }
321
+
322
+ const delayMs = EMBEDDING_RETRY_DELAYS_MS[attempt];
323
+ const errorMsg = error instanceof Error ? error.message : String(error);
324
+ logWarn(`Transient embedding error, retrying in ${delayMs}ms (attempt ${attempt + 1}/${EMBEDDING_RETRY_DELAYS_MS.length})`, errorMsg);
325
+ attempt += 1;
326
+ await sleep(delayMs);
327
+ }
328
+ }
329
+ }
330
+
331
+ private async embedSingle(text: string, task?: string): Promise<number[]> {
332
+ if (!text || text.trim().length === 0) {
333
+ throw new Error("Cannot embed empty text");
334
+ }
335
+
336
+ // Check cache first
337
+ const cached = this._cache.get(text, task);
338
+ if (cached) return cached;
339
+
340
+ try {
341
+ const response = await this.createEmbedding(text, task);
342
+ const embedding = response.data[0]?.embedding as number[] | undefined;
343
+ if (!embedding) {
344
+ throw new Error("No embedding returned from provider");
345
+ }
346
+
347
+ this.validateEmbedding(embedding);
348
+ this._cache.set(text, task, embedding);
349
+ return embedding;
350
+ } catch (error) {
351
+ // Check if this is a context length exceeded error and try chunking
352
+ const errorMsg = error instanceof Error ? error.message : String(error);
353
+ const isContextError = /context|too long|exceed|length/i.test(errorMsg);
354
+
355
+ if (isContextError && this._autoChunk) {
356
+ try {
357
+ logInfo(`Document exceeded context limit (${errorMsg}), attempting chunking...`);
358
+ const chunkResult = smartChunk(text, this._model);
359
+
360
+ if (chunkResult.chunks.length === 0) {
361
+ throw new Error(`Failed to chunk document: ${errorMsg}`);
362
+ }
363
+
364
+ // Embed all chunks in parallel
365
+ logInfo(`Split document into ${chunkResult.chunkCount} chunks for embedding`);
366
+ const chunkEmbeddings = await Promise.all(
367
+ chunkResult.chunks.map(async (chunk, idx) => {
368
+ try {
369
+ const embedding = await this.embedSingle(chunk, task);
370
+ return { embedding };
371
+ } catch (chunkError) {
372
+ logWarn(`Failed to embed chunk ${idx}:`, chunkError);
373
+ throw chunkError;
374
+ }
375
+ })
376
+ );
377
+
378
+ // Compute average embedding across chunks
379
+ const avgEmbedding = chunkEmbeddings.reduce(
380
+ (sum, { embedding }) => {
381
+ for (let i = 0; i < embedding.length; i++) {
382
+ sum[i] += embedding[i];
383
+ }
384
+ return sum;
385
+ },
386
+ new Array(this.dimensions).fill(0)
387
+ );
388
+
389
+ const finalEmbedding = avgEmbedding.map(v => v / chunkEmbeddings.length);
390
+
391
+ // Cache the result for the original text (using its hash)
392
+ this._cache.set(text, task, finalEmbedding);
393
+ logInfo(`Successfully embedded long document as ${chunkEmbeddings.length} averaged chunks`);
394
+
395
+ return finalEmbedding;
396
+ } catch (chunkError) {
397
+ // If chunking fails, throw the original error
398
+ logWarn(`Chunking failed, using original error:`, chunkError);
399
+ throw new Error(`Failed to generate embedding: ${errorMsg}`, { cause: error });
400
+ }
401
+ }
402
+
403
+ if (error instanceof Error) {
404
+ throw new Error(`Failed to generate embedding: ${error.message}`, { cause: error });
405
+ }
406
+ throw new Error(`Failed to generate embedding: ${String(error)}`);
407
+ }
408
+ }
409
+
410
+ private async embedMany(texts: string[], task?: string): Promise<number[][]> {
411
+ if (!texts || texts.length === 0) {
412
+ return [];
413
+ }
414
+
415
+ // Filter out empty texts and track indices
416
+ const validTexts: string[] = [];
417
+ const validIndices: number[] = [];
418
+
419
+ texts.forEach((text, index) => {
420
+ if (text && text.trim().length > 0) {
421
+ validTexts.push(text);
422
+ validIndices.push(index);
423
+ }
424
+ });
425
+
426
+ if (validTexts.length === 0) {
427
+ return texts.map(() => []);
428
+ }
429
+
430
+ try {
431
+ const response = await this.createEmbedding(validTexts, task);
432
+
433
+ // Create result array with proper length
434
+ const results: number[][] = new Array(texts.length);
435
+
436
+ // Fill in embeddings for valid texts
437
+ response.data.forEach((item, idx) => {
438
+ const originalIndex = validIndices[idx];
439
+ const embedding = item.embedding as number[];
440
+
441
+ this.validateEmbedding(embedding);
442
+ results[originalIndex] = embedding;
443
+ });
444
+
445
+ // Fill empty arrays for invalid texts
446
+ for (let i = 0; i < texts.length; i++) {
447
+ if (!results[i]) {
448
+ results[i] = [];
449
+ }
450
+ }
451
+
452
+ return results;
453
+ } catch (error) {
454
+ // Check if this is a context length exceeded error and try chunking each text
455
+ const errorMsg = error instanceof Error ? error.message : String(error);
456
+ const isContextError = /context|too long|exceed|length/i.test(errorMsg);
457
+
458
+ if (isContextError && this._autoChunk) {
459
+ try {
460
+ logInfo(`Batch embedding failed with context error, attempting chunking...`);
461
+
462
+ const chunkResults = await Promise.all(
463
+ validTexts.map(async (text, idx) => {
464
+ const chunkResult = smartChunk(text, this._model);
465
+ if (chunkResult.chunks.length === 0) {
466
+ throw new Error("Chunker produced no chunks");
467
+ }
468
+
469
+ // Embed all chunks in parallel, then average.
470
+ const embeddings = await Promise.all(
471
+ chunkResult.chunks.map((chunk) => this.embedSingle(chunk, task))
472
+ );
473
+
474
+ const avgEmbedding = embeddings.reduce(
475
+ (sum, emb) => {
476
+ for (let i = 0; i < emb.length; i++) {
477
+ sum[i] += emb[i];
478
+ }
479
+ return sum;
480
+ },
481
+ new Array(this.dimensions).fill(0)
482
+ );
483
+
484
+ const finalEmbedding = avgEmbedding.map((v) => v / embeddings.length);
485
+
486
+ // Cache the averaged embedding for the original (long) text.
487
+ this._cache.set(text, task, finalEmbedding);
488
+
489
+ return { embedding: finalEmbedding, index: validIndices[idx] };
490
+ })
491
+ );
492
+
493
+ logInfo(`Successfully chunked and embedded ${chunkResults.length} long documents`);
494
+
495
+ // Build results array
496
+ const results: number[][] = new Array(texts.length);
497
+ chunkResults.forEach(({ embedding, index }) => {
498
+ if (embedding.length > 0) {
499
+ this.validateEmbedding(embedding);
500
+ results[index] = embedding;
501
+ } else {
502
+ results[index] = [];
503
+ }
504
+ });
505
+
506
+ // Fill empty arrays for invalid texts
507
+ for (let i = 0; i < texts.length; i++) {
508
+ if (!results[i]) {
509
+ results[i] = [];
510
+ }
511
+ }
512
+
513
+ return results;
514
+ } catch (chunkError) {
515
+ throw new Error(`Failed to embed documents after chunking attempt: ${errorMsg}`);
516
+ }
517
+ }
518
+
519
+ if (error instanceof Error) {
520
+ throw new Error(`Failed to generate batch embeddings: ${error.message}`, { cause: error });
521
+ }
522
+ throw new Error(`Failed to generate batch embeddings: ${String(error)}`);
523
+ }
524
+ }
525
+
526
+ get model(): string {
527
+ return this._model;
528
+ }
529
+
530
+ // Test connection and validate configuration
531
+ async test(): Promise<{ success: boolean; error?: string; dimensions?: number }> {
532
+ try {
533
+ const testEmbedding = await this.embedPassage("test");
534
+ return {
535
+ success: true,
536
+ dimensions: testEmbedding.length,
537
+ };
538
+ } catch (error) {
539
+ return {
540
+ success: false,
541
+ error: error instanceof Error ? error.message : String(error),
542
+ };
543
+ }
544
+ }
545
+
546
+ get cacheStats() {
547
+ return this._cache.stats;
548
+ }
549
+ }
550
+
551
+ // ============================================================================
552
+ // Factory Function
553
+ // ============================================================================
554
+
555
+ export function createEmbedder(config: EmbeddingConfig): Embedder {
556
+ return new Embedder(config);
557
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Entity Resolver — normalize tech entity aliases to canonical names.
3
+ *
4
+ * Borrowed from UltraMemory's entity-resolver.ts, adapted for RecallNest:
5
+ * - Extended with Chinese aliases (RecallNest users are bilingual)
6
+ * - Hooked into query-expander as a pre-normalization step
7
+ * - Also usable at capture time to normalize entities before canonical key generation
8
+ *
9
+ * Pure mapping logic, zero API calls.
10
+ */
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Built-in aliases (lowercase key → canonical name)
14
+ // ---------------------------------------------------------------------------
15
+
16
+ export const BUILTIN_ALIASES: Record<string, string> = {
17
+ // Programming languages
18
+ "ts": "typescript", "typescript": "typescript",
19
+ "js": "javascript", "javascript": "javascript",
20
+ "py": "python", "python": "python",
21
+ "rb": "ruby", "ruby": "ruby",
22
+ "rs": "rust", "rust": "rust",
23
+ "go": "golang", "golang": "golang",
24
+ "cs": "csharp", "c#": "csharp",
25
+
26
+ // Runtimes
27
+ "node.js": "nodejs", "node": "nodejs", "nodejs": "nodejs",
28
+ "bun": "bun", "deno": "deno",
29
+
30
+ // Frameworks
31
+ "react.js": "react", "reactjs": "react", "react": "react",
32
+ "vue.js": "vue", "vuejs": "vue", "vue": "vue",
33
+ "next.js": "nextjs", "next": "nextjs", "nextjs": "nextjs",
34
+ "express.js": "express", "express": "express",
35
+ "angular.js": "angular", "angularjs": "angular", "angular": "angular",
36
+ "svelte": "svelte", "sveltekit": "sveltekit",
37
+
38
+ // Infrastructure
39
+ "k8s": "kubernetes", "kubernetes": "kubernetes",
40
+ "pg": "postgresql", "postgres": "postgresql", "postgresql": "postgresql",
41
+ "mongo": "mongodb", "mongodb": "mongodb",
42
+ "redis": "redis",
43
+ "docker": "docker",
44
+ "docker-compose": "docker compose", "docker compose": "docker compose",
45
+ "tf": "terraform", "terraform": "terraform",
46
+ "gcp": "google cloud platform",
47
+ "aws": "amazon web services",
48
+
49
+ // AI/ML
50
+ "gpt": "openai gpt", "chatgpt": "openai gpt",
51
+ "claude": "anthropic claude", "anthropic": "anthropic claude",
52
+ "gemini": "google gemini",
53
+ "llm": "large language model",
54
+ "rag": "retrieval augmented generation",
55
+ "lancedb": "lancedb",
56
+ "pt": "pytorch", "pytorch": "pytorch",
57
+ "tf.js": "tensorflow", "tensorflow": "tensorflow",
58
+ "hf": "hugging face", "huggingface": "hugging face",
59
+
60
+ // Project-specific
61
+ "recallnest": "recallnest", "recall nest": "recallnest", "recall-nest": "recallnest",
62
+ "openclaw": "openclaw", "open claw": "openclaw",
63
+ "ultramemory": "ultramemory", "ultra memory": "ultramemory",
64
+ "cc": "claude code", "claude code": "claude code",
65
+ "mcp": "model context protocol",
66
+ };
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Resolver class
70
+ // ---------------------------------------------------------------------------
71
+
72
+ export class EntityResolver {
73
+ private aliases: Map<string, string>;
74
+
75
+ constructor(
76
+ builtinAliases: Record<string, string> = BUILTIN_ALIASES,
77
+ userAliases?: Record<string, string>,
78
+ ) {
79
+ this.aliases = new Map();
80
+ for (const [k, v] of Object.entries(builtinAliases)) {
81
+ this.aliases.set(k.toLowerCase(), v);
82
+ }
83
+ if (userAliases) {
84
+ for (const [k, v] of Object.entries(userAliases)) {
85
+ this.aliases.set(k.toLowerCase(), v);
86
+ }
87
+ }
88
+ }
89
+
90
+ /** Resolve a single term to its canonical form. Returns the term as-is if no alias found. */
91
+ resolve(term: string): string {
92
+ return this.aliases.get(term.toLowerCase().trim()) ?? term;
93
+ }
94
+
95
+ /** Resolve all recognizable entities in a text string. Returns text with aliases replaced. */
96
+ resolveText(text: string): string {
97
+ let result = text;
98
+ // Sort aliases by length descending to match longer aliases first
99
+ const sorted = [...this.aliases.entries()].sort((a, b) => b[0].length - a[0].length);
100
+ for (const [alias, canonical] of sorted) {
101
+ if (alias === canonical) continue; // skip identity mappings
102
+ const escaped = alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
103
+ // Always use word boundary to prevent matching inside other words
104
+ // (e.g. "pt" inside "typescript")
105
+ const re = new RegExp(`\\b${escaped}\\b`, "gi");
106
+ result = result.replace(re, canonical);
107
+ }
108
+ return result;
109
+ }
110
+
111
+ /** Check if a term has a known alias. */
112
+ hasAlias(term: string): boolean {
113
+ return this.aliases.has(term.toLowerCase().trim());
114
+ }
115
+
116
+ /** Get all known aliases. */
117
+ get size(): number {
118
+ return this.aliases.size;
119
+ }
120
+ }
121
+
122
+ /** Default singleton resolver with built-in aliases. */
123
+ export const defaultResolver = new EntityResolver();
124
+
125
+ /**
126
+ * Resolve entities in a query string using the default resolver.
127
+ * Intended to be called before query expansion in the search pipeline.
128
+ */
129
+ export function resolveQueryEntities(query: string): string {
130
+ return defaultResolver.resolveText(query);
131
+ }