memory-lancedb-pro 1.0.7 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.10
4
+
5
+ - Fix: avoid blocking OpenClaw gateway startup on external network calls by running startup self-checks in the background with timeouts.
6
+
7
+ ## 1.0.9
8
+
9
+ - Change: update default `retrieval.rerankModel` to `jina-reranker-v3` (still fully configurable).
10
+
11
+ ## 1.0.8
12
+
13
+ - Add: JSONL distill extractor supports optional agent allowlist via env var `OPENCLAW_JSONL_DISTILL_ALLOWED_AGENT_IDS` (default off / compatible).
14
+
3
15
  ## 1.0.7
4
16
 
5
17
  - Fix: resolve `agentId` from hook context (`ctx?.agentId`) for `before_agent_start` and `agent_end`, restoring per-agent scope isolation when using multi-agent setups.
package/README.md CHANGED
@@ -161,6 +161,32 @@ Filters out low-quality content at both auto-capture and tool-store stages:
161
161
 
162
162
  ## Installation
163
163
 
164
+ ### AI-safe install notes (anti-hallucination)
165
+
166
+ If you are following this README using an AI assistant, **do not assume defaults**. Always run these commands first and use the real output:
167
+
168
+ ```bash
169
+ openclaw config get agents.defaults.workspace
170
+ openclaw config get plugins.load.paths
171
+ openclaw config get plugins.slots.memory
172
+ openclaw config get plugins.entries.memory-lancedb-pro
173
+ ```
174
+
175
+ Recommendations:
176
+ - Prefer **absolute paths** in `plugins.load.paths` unless you have confirmed the active workspace.
177
+ - If you use `${JINA_API_KEY}` (or any `${...}` variable) in config, ensure the **Gateway service process** has that environment variable (system services often do **not** inherit your interactive shell env).
178
+ - After changing plugin config, run `openclaw gateway restart`.
179
+
180
+ ### Jina API keys (embedding + rerank)
181
+
182
+ - **Embedding**: set `embedding.apiKey` to your Jina key (recommended: use an env var like `${JINA_API_KEY}`).
183
+ - **Rerank** (when `retrieval.rerankProvider: "jina"`): you can typically use the **same** Jina key for `retrieval.rerankApiKey`.
184
+ - If you use a different rerank provider (`siliconflow`, `pinecone`, etc.), `retrieval.rerankApiKey` should be that provider’s key.
185
+
186
+ Key storage guidance:
187
+ - Avoid committing secrets into git.
188
+ - Using `${...}` env vars is fine, but make sure the **Gateway service process** has those env vars (system services often do not inherit your interactive shell environment).
189
+
164
190
  ### What is the “OpenClaw workspace”?
165
191
 
166
192
  In OpenClaw, the **agent workspace** is the agent’s working directory (default: `~/.openclaw/workspace`).
@@ -168,7 +194,9 @@ According to the docs, the workspace is the **default cwd**, and **relative path
168
194
 
169
195
  > Note: OpenClaw configuration typically lives under `~/.openclaw/openclaw.json` (separate from the workspace).
170
196
 
171
- **Common mistake:** cloning the plugin somewhere else, while keeping `plugins.load.paths: ["plugins/memory-lancedb-pro"]` (a **relative path**). In that case OpenClaw will look for `plugins/memory-lancedb-pro` under your **workspace** and fail to load it.
197
+ **Common mistake:** cloning the plugin somewhere else, while keeping a **relative path** like `plugins.load.paths: ["plugins/memory-lancedb-pro"]`. Relative paths can be resolved against different working directories depending on how the Gateway is started.
198
+
199
+ To avoid ambiguity, use an **absolute path** (Option B) or clone into `<workspace>/plugins/` (Option A) and keep your config consistent.
172
200
 
173
201
  ### Option A (recommended): clone into `plugins/` under your workspace
174
202
 
@@ -285,8 +313,8 @@ openclaw config get plugins.slots.memory
285
313
  "bm25Weight": 0.3,
286
314
  "minScore": 0.3,
287
315
  "rerank": "cross-encoder",
288
- "rerankApiKey": "jina_xxx",
289
- "rerankModel": "jina-reranker-v2-base-multilingual",
316
+ "rerankApiKey": "${JINA_API_KEY}",
317
+ "rerankModel": "jina-reranker-v3",
290
318
  "rerankEndpoint": "https://api.jina.ai/v1/rerank",
291
319
  "rerankProvider": "jina",
292
320
  "candidatePoolSize": 20,
@@ -334,7 +362,7 @@ Cross-encoder reranking supports multiple providers via `rerankProvider`:
334
362
 
335
363
  | Provider | `rerankProvider` | Endpoint | Example Model |
336
364
  |----------|-----------------|----------|---------------|
337
- | **Jina** (default) | `jina` | `https://api.jina.ai/v1/rerank` | `jina-reranker-v2-base-multilingual` |
365
+ | **Jina** (default) | `jina` | `https://api.jina.ai/v1/rerank` | `jina-reranker-v3` |
338
366
  | **SiliconFlow** (free tier available) | `siliconflow` | `https://api.siliconflow.com/v1/rerank` | `BAAI/bge-reranker-v2-m3`, `Qwen/Qwen3-Reranker-8B` |
339
367
  | **Pinecone** | `pinecone` | `https://api.pinecone.io/rerank` | `bge-reranker-v2-m3` |
340
368
 
@@ -382,7 +410,21 @@ OpenClaw already persists **full session transcripts** as JSONL files:
382
410
 
383
411
  This plugin focuses on **high-quality long-term memory**. If you dump raw transcripts into LanceDB, retrieval quality quickly degrades.
384
412
 
385
- Instead, you can run an **hourly distiller** that:
413
+ Instead, **recommended (2026-02+)** is a **non-blocking `/new` pipeline**:
414
+
415
+ - Trigger: `command:new` (you type `/new`)
416
+ - Hook: enqueue a tiny JSON task file (fast; no LLM calls inside the hook)
417
+ - Worker: a user-level systemd service watches the inbox and runs **Gemini Map-Reduce** on the session JSONL transcript
418
+ - Store: writes **0–20** high-signal, atomic lessons into LanceDB Pro via `openclaw memory-pro import`
419
+ - Keywords: each memory includes `Keywords (zh)` with a simple taxonomy (Entity + Action + Symptom). Entity keywords must be copied verbatim from the transcript (no hallucinated project names).
420
+ - Notify: optional Telegram/Discord notification (even if 0 lessons)
421
+
422
+ See the self-contained example files in:
423
+ - `examples/new-session-distill/`
424
+
425
+ ---
426
+
427
+ Legacy option: an **hourly distiller** cron that:
386
428
 
387
429
  1) Incrementally reads only the **newly appended tail** of each session JSONL (byte-offset cursor)
388
430
  2) Filters noise (tool output, injected `<relevant-memories>`, logs, boilerplate)
@@ -414,6 +456,19 @@ The script is **safe**: it never modifies session logs.
414
456
 
415
457
  By default it skips historical reset snapshots (`*.reset.*`) and excludes the distiller agent itself (`memory-distiller`) to prevent self-ingestion loops.
416
458
 
459
+ ### Optional: restrict distillation sources (allowlist)
460
+
461
+ By default, the extractor scans **all agents** (except `memory-distiller`).
462
+
463
+ If you want higher signal (e.g., only distill from your main assistant + coding bot), set:
464
+
465
+ ```bash
466
+ export OPENCLAW_JSONL_DISTILL_ALLOWED_AGENT_IDS="main,code-agent"
467
+ ```
468
+
469
+ - Unset / empty / `*` / `all` → allow all agents (default)
470
+ - Comma-separated list → only those agents are scanned
471
+
417
472
  ### Recommended setup (dedicated distiller agent)
418
473
 
419
474
  #### 1) Create a dedicated agent
package/README_CN.md CHANGED
@@ -112,7 +112,7 @@ Query → BM25 FTS ─────┘
112
112
 
113
113
  ### 2. 跨编码器 Rerank
114
114
 
115
- - **Jina Reranker API**: `jina-reranker-v2-base-multilingual`(5s 超时保护)
115
+ - **Jina Reranker API**: `jina-reranker-v3`(5s 超时保护)
116
116
  - **混合评分**: 60% cross-encoder score + 40% 原始融合分
117
117
  - **降级策略**: API 失败时回退到 cosine similarity rerank
118
118
 
@@ -162,6 +162,32 @@ Query → BM25 FTS ─────┘
162
162
 
163
163
  ## 安装
164
164
 
165
+ ### AI 安装指引(防幻觉版)
166
+
167
+ 如果你是用 AI 按 README 操作,**不要假设任何默认值**。请先运行以下命令,并以真实输出为准:
168
+
169
+ ```bash
170
+ openclaw config get agents.defaults.workspace
171
+ openclaw config get plugins.load.paths
172
+ openclaw config get plugins.slots.memory
173
+ openclaw config get plugins.entries.memory-lancedb-pro
174
+ ```
175
+
176
+ 建议:
177
+ - `plugins.load.paths` 建议优先用**绝对路径**(除非你已确认当前 workspace)。
178
+ - 如果配置里使用 `${JINA_API_KEY}`(或任何 `${...}` 变量),务必确保运行 Gateway 的**服务进程环境**里真的有这些变量(systemd/launchd/docker 通常不会继承你终端的 export)。
179
+ - 修改插件配置后,运行 `openclaw gateway restart` 使其生效。
180
+
181
+ ### Jina API Key(Embedding + Rerank)如何填写
182
+
183
+ - **Embedding**:将 `embedding.apiKey` 设置为你的 Jina key(推荐用环境变量 `${JINA_API_KEY}`)。
184
+ - **Rerank**(当 `retrieval.rerankProvider: "jina"`):通常可以直接复用同一个 Jina key,填到 `retrieval.rerankApiKey`。
185
+ - 如果你选择了其它 rerank provider(如 `siliconflow` / `pinecone`),则 `retrieval.rerankApiKey` 应填写对应提供商的 key。
186
+
187
+ Key 存储建议:
188
+ - 不要把 key 提交到 git。
189
+ - 使用 `${...}` 环境变量没问题,但务必确保运行 Gateway 的**服务进程环境**里真的有该变量(systemd/launchd/docker 往往不会继承你终端的 export)。
190
+
165
191
  ### 什么是 “OpenClaw workspace”?
166
192
 
167
193
  在 OpenClaw 中,**agent workspace(工作区)** 是 Agent 的工作目录(默认:`~/.openclaw/workspace`)。
@@ -169,7 +195,9 @@ Query → BM25 FTS ─────┘
169
195
 
170
196
  > 说明:OpenClaw 的配置文件通常在 `~/.openclaw/openclaw.json`,与 workspace 是分开的。
171
197
 
172
- **最常见的安装错误:** 把插件 clone 到别的目录,但在配置里仍然写 `"paths": ["plugins/memory-lancedb-pro"]`(这是**相对路径**)。OpenClaw 会去 workspace 下找 `plugins/memory-lancedb-pro`,导致加载失败,于是出现“安装位置不对”的反馈。
198
+ **最常见的安装错误:** 把插件 clone 到别的目录,但在配置里仍然写类似 `"paths": ["plugins/memory-lancedb-pro"]` 的**相对路径**。相对路径的解析基准会受 Gateway 启动方式/工作目录影响,容易指向错误位置。
199
+
200
+ 为避免歧义:建议用**绝对路径**(方案 B),或把插件放在 `<workspace>/plugins/`(方案 A)并保持配置一致。
173
201
 
174
202
  ### 方案 A(推荐):克隆到 workspace 的 `plugins/` 目录下
175
203
 
@@ -286,8 +314,8 @@ openclaw config get plugins.slots.memory
286
314
  "bm25Weight": 0.3,
287
315
  "minScore": 0.3,
288
316
  "rerank": "cross-encoder",
289
- "rerankApiKey": "jina_xxx",
290
- "rerankModel": "jina-reranker-v2-base-multilingual",
317
+ "rerankApiKey": "${JINA_API_KEY}",
318
+ "rerankModel": "jina-reranker-v3",
291
319
  "candidatePoolSize": 20,
292
320
  "recencyHalfLifeDays": 14,
293
321
  "recencyWeight": 0.1,
@@ -337,7 +365,21 @@ OpenClaw 会把每个 Agent 的完整会话自动落盘为 JSONL:
337
365
 
338
366
  但 JSONL 含大量噪声(tool 输出、系统块、重复回调等),**不建议直接把原文塞进 LanceDB**。
339
367
 
340
- 本插件提供一个安全的 extractor 脚本 `scripts/jsonl_distill.py`,配合 OpenClaw `cron` + 独立 distiller agent,实现“增量蒸馏 → 高质量记忆入库”:
368
+ **推荐方案(2026-02+)**:使用 **/new 非阻塞沉淀管线**(Hooks + systemd worker),在你执行 `/new` 时异步提取高价值经验并写入 LanceDB Pro:
369
+
370
+ - 触发:`command:new`(你在聊天里发送 `/new`)
371
+ - Hook:只投递一个很小的 task.json(毫秒级,不调用 LLM,不阻塞 `/new`)
372
+ - Worker:systemd 常驻进程监听队列,读取 session `.jsonl`,用 Gemini **Map-Reduce** 抽取 0~20 条高信噪比记忆
373
+ - 写入:通过 `openclaw memory-pro import` 写入 LanceDB Pro(插件内部仍会 embedding + 查重)
374
+ - 中文关键词:每条记忆包含 `Keywords (zh)`,并遵循三要素(实体/动作/症状)。其中“实体关键词”必须从 transcript 原文逐字拷贝(禁止编造项目名)。
375
+ - 通知:可选(可做到即使 0 条也通知)
376
+
377
+ 示例文件:
378
+ - `examples/new-session-distill/`
379
+
380
+ ---
381
+
382
+ Legacy 方案:本插件也提供一个安全的 extractor 脚本 `scripts/jsonl_distill.py`,配合 OpenClaw 的 `cron` + 独立 distiller agent,实现“增量蒸馏 → 高质量记忆入库”:(适合不依赖 `/new` 的全自动场景)
341
383
 
342
384
  - 只读取每个 JSONL 文件**新增尾巴**(byte offset cursor),避免重复和 token 浪费
343
385
  - 生成一个小型 batch JSON
@@ -357,6 +399,19 @@ OpenClaw 会把每个 Agent 的完整会话自动落盘为 JSONL:
357
399
 
358
400
  > 脚本只读 session JSONL,不会修改原始日志。
359
401
 
402
+ ### (可选)启用 Agent 来源白名单(提高信噪比)
403
+
404
+ 默认情况下,extractor 会扫描 **所有 Agent**(但会排除 `memory-distiller` 自身,防止自我吞噬)。
405
+
406
+ 如果你只想从某些 Agent 蒸馏(例如只蒸馏 `main` + `code-agent`),可以设置环境变量:
407
+
408
+ ```bash
409
+ export OPENCLAW_JSONL_DISTILL_ALLOWED_AGENT_IDS="main,code-agent"
410
+ ```
411
+
412
+ - 不设置 / 空 / `*` / `all`:扫描全部(默认)
413
+ - 逗号分隔列表:只扫描列表内 agentId
414
+
360
415
  ### 推荐部署(独立 distiller agent)
361
416
 
362
417
  #### 1)创建 distiller agent(示例用 gpt-5.2)
@@ -0,0 +1,19 @@
1
+ # New Session Distillation (Recommended)
2
+
3
+ This example shows a **non-blocking /new distillation pipeline**:
4
+
5
+ - Trigger: `command:new` (when you type `/new`)
6
+ - Hook: enqueue a small JSON task file (fast, no LLM calls)
7
+ - Worker: a user-level systemd service watches the inbox and runs **Gemini Map-Reduce** over the session JSONL transcript
8
+ - Storage: write high-signal, atomic lessons into LanceDB Pro via `openclaw memory-pro import`
9
+ - Notify: send a notification message (optional)
10
+
11
+ Files included:
12
+ - `hook/enqueue-lesson-extract/` — OpenClaw workspace hook
13
+ - `worker/lesson-extract-worker.mjs` — Map-Reduce extractor + importer + notifier
14
+ - `worker/systemd/lesson-extract-worker.service` — user systemd unit
15
+
16
+ You must provide:
17
+ - `GEMINI_API_KEY` in an env file loaded by systemd
18
+
19
+ Install steps are documented in the main repo README.
@@ -0,0 +1,21 @@
1
+ ---
2
+ name: enqueue-lesson-extract
3
+ description: "Enqueue a lesson-extraction task on /new (async Map-Reduce → LanceDB Pro)"
4
+ metadata:
5
+ {
6
+ "openclaw": {
7
+ "emoji": "🧾",
8
+ "events": ["command:new"]
9
+ }
10
+ }
11
+ ---
12
+
13
+ # Enqueue Lesson Extract Hook
14
+
15
+ Writes a small JSON task file to a queue directory when `/new` is issued.
16
+
17
+ This is intentionally fast and non-blocking. A separate systemd worker consumes tasks and:
18
+ - reads the session JSONL transcript
19
+ - runs Map-Reduce extraction with Gemini Flash
20
+ - writes high-signal, deduped lessons into LanceDB Pro
21
+ - sends a notification (optional)
@@ -0,0 +1,102 @@
1
+ import type { HookHandler } from "../../src/hooks/hooks.js";
2
+ import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
3
+ import { basename, dirname, join } from "node:path";
4
+ import { createHash } from "node:crypto";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ function findSessionFile(event: any): string | null {
8
+ const prev = event.context?.previousSessionEntry;
9
+ const curr = event.context?.sessionEntry;
10
+
11
+ const candidates = [prev?.sessionFile, curr?.sessionFile].filter(Boolean);
12
+
13
+ for (const file of candidates) {
14
+ if (file && existsSync(file)) return file;
15
+ }
16
+
17
+ // If the file was renamed to .reset.*, look for the latest one
18
+ for (const file of candidates) {
19
+ if (!file) continue;
20
+ const dir = dirname(file);
21
+ const base = basename(file);
22
+ const resetPrefix = `${base}.reset.`;
23
+ try {
24
+ const resetFiles = readdirSync(dir)
25
+ .filter((name: string) => name.startsWith(resetPrefix))
26
+ .sort();
27
+ if (resetFiles.length > 0) return join(dir, resetFiles[resetFiles.length - 1]);
28
+ } catch {
29
+ // ignore
30
+ }
31
+ }
32
+
33
+ return null;
34
+ }
35
+
36
+ function sha1(input: string): string {
37
+ return createHash("sha1").update(input).digest("hex");
38
+ }
39
+
40
+ function repoRoot(): string {
41
+ // examples/new-session-distill/hook/enqueue-lesson-extract/handler.ts -> up to repo root
42
+ const here = dirname(fileURLToPath(import.meta.url));
43
+ return join(here, "..", "..", "..", "..", "..");
44
+ }
45
+
46
+ const handler: HookHandler = async (event) => {
47
+ if (event.type !== "command" || event.action !== "new") return;
48
+
49
+ try {
50
+ const sessionKey = event.sessionKey || "unknown";
51
+ const ctxEntry = (event.context?.previousSessionEntry || event.context?.sessionEntry) as any;
52
+ const sessionId = (ctxEntry?.sessionId as string) || "unknown";
53
+ const source = (event.context?.commandSource as string) || "unknown";
54
+
55
+ const sessionFile = findSessionFile(event);
56
+ if (!sessionFile) {
57
+ console.error("[enqueue-lesson-extract] No session file found; skipping");
58
+ return;
59
+ }
60
+
61
+ const root = repoRoot();
62
+ const queueDir = join(root, "workspaces", "main", "tasks", "lesson-extract", "inbox");
63
+ mkdirSync(queueDir, { recursive: true });
64
+
65
+ const ts = new Date(event.timestamp || Date.now()).toISOString();
66
+ const taskId = sha1([sessionKey, sessionId, sessionFile, ts].join("|"));
67
+
68
+ const task = {
69
+ taskId,
70
+ agentId: "main",
71
+ scope: "agent:main",
72
+ event: {
73
+ type: "command:new",
74
+ timestamp: ts,
75
+ sessionKey,
76
+ source,
77
+ },
78
+ session: {
79
+ sessionId,
80
+ sessionFile,
81
+ },
82
+ extract: {
83
+ maxFinal: 20,
84
+ mapChunkChars: 12000,
85
+ mapOverlapMsgs: 10,
86
+ },
87
+ };
88
+
89
+ const safeTs = ts.replace(/[:.]/g, "-");
90
+ const filename = `${safeTs}-${taskId.slice(0, 8)}.json`;
91
+ const outPath = join(queueDir, filename);
92
+
93
+ writeFileSync(outPath, JSON.stringify(task, null, 2) + "\n", "utf-8");
94
+ } catch (err) {
95
+ console.error(
96
+ "[enqueue-lesson-extract] Error:",
97
+ err instanceof Error ? err.message : String(err)
98
+ );
99
+ }
100
+ };
101
+
102
+ export default handler;
@@ -0,0 +1,412 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * lesson-extract-worker (example)
5
+ *
6
+ * Watches a repo-backed inbox for lesson extraction tasks and processes them asynchronously:
7
+ * - reads session JSONL transcript (streaming)
8
+ * - Map: per-chunk extraction via Gemini (native API)
9
+ * - Reduce: merge/dedupe/score -> 0..20 lessons
10
+ * - writes to LanceDB Pro via `openclaw memory-pro import`
11
+ * - sends Telegram notification via `openclaw message send` (optional)
12
+ */
13
+
14
+ import fs from "node:fs";
15
+ import fsp from "node:fs/promises";
16
+ import path from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+ import { spawn } from "node:child_process";
19
+ import readline from "node:readline";
20
+
21
+ const __filename = fileURLToPath(import.meta.url);
22
+ const __dirname = path.dirname(__filename);
23
+
24
+ // In your deployment, set LESSON_QUEUE_ROOT to your workspace queue.
25
+ // By default we assume repo layout similar to OpenClaw-Memory.
26
+ const REPO_ROOT = path.resolve(__dirname, "..", "..", "..", "..", "..");
27
+ const QUEUE_ROOT = process.env.LESSON_QUEUE_ROOT || path.join(REPO_ROOT, "workspaces", "main", "tasks", "lesson-extract");
28
+
29
+ const INBOX = path.join(QUEUE_ROOT, "inbox");
30
+ const PROCESSING = path.join(QUEUE_ROOT, "processing");
31
+ const DONE = path.join(QUEUE_ROOT, "done");
32
+ const ERROR = path.join(QUEUE_ROOT, "error");
33
+
34
+ const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
35
+ const GEMINI_MODEL = process.env.GEMINI_MODEL || "gemini-3-flash-preview";
36
+
37
+ const ONCE = process.argv.includes("--once");
38
+
39
+ function ensureDirs() {
40
+ for (const d of [INBOX, PROCESSING, DONE, ERROR]) {
41
+ fs.mkdirSync(d, { recursive: true });
42
+ }
43
+ }
44
+
45
+ function nowIso() {
46
+ return new Date().toISOString();
47
+ }
48
+
49
+ function run(cmd, args, opts = {}) {
50
+ return new Promise((resolve) => {
51
+ const child = spawn(cmd, args, { ...opts, stdio: ["ignore", "pipe", "pipe"] });
52
+ let out = "";
53
+ let err = "";
54
+ child.stdout.on("data", (d) => (out += d.toString("utf-8")));
55
+ child.stderr.on("data", (d) => (err += d.toString("utf-8")));
56
+ child.on("close", (code) => resolve({ code: code ?? 0, out, err }));
57
+ });
58
+ }
59
+
60
+ function safeJsonParse(text) {
61
+ try {
62
+ return JSON.parse(text);
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ function normalizeText(s) {
69
+ return (s || "")
70
+ .trim()
71
+ .replace(/\s+/g, " ")
72
+ .replace(/[“”]/g, '"')
73
+ .replace(/[‘’]/g, "'")
74
+ .toLowerCase();
75
+ }
76
+
77
+ function detectLang(text) {
78
+ const s = text || "";
79
+ const cjk = (s.match(/[\u4e00-\u9fff]/g) || []).length;
80
+ const latin = (s.match(/[A-Za-z]/g) || []).length;
81
+ if (cjk > latin * 0.8) return "zh";
82
+ if (latin > cjk * 0.8) return "en";
83
+ return "mixed";
84
+ }
85
+
86
+ async function* iterJsonlMessages(sessionFile) {
87
+ const stream = fs.createReadStream(sessionFile, { encoding: "utf-8" });
88
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
89
+
90
+ let id = 0;
91
+ for await (const line of rl) {
92
+ if (!line) continue;
93
+ const obj = safeJsonParse(line);
94
+ if (!obj || obj.type !== "message") continue;
95
+ const m = obj.message;
96
+ if (!m || (m.role !== "user" && m.role !== "assistant")) continue;
97
+
98
+ let text = "";
99
+ if (typeof m.content === "string") {
100
+ text = m.content;
101
+ } else if (Array.isArray(m.content)) {
102
+ text = m.content
103
+ .filter((c) => c && c.type === "text" && c.text)
104
+ .map((c) => c.text)
105
+ .join("\n");
106
+ }
107
+ text = (text || "").trim();
108
+ if (!text) continue;
109
+
110
+ id++;
111
+ yield {
112
+ id,
113
+ role: m.role,
114
+ timestamp: obj.timestamp || "",
115
+ text,
116
+ };
117
+ }
118
+ }
119
+
120
+ async function buildChunksFromJsonl(sessionFile, { maxChars = 12000, overlapMsgs = 10, maxChunks = 200 } = {}) {
121
+ const chunks = [];
122
+ let chunk = [];
123
+ let size = 0;
124
+
125
+ const sampleTexts = [];
126
+
127
+ for await (const m of iterJsonlMessages(sessionFile)) {
128
+ if (sampleTexts.length < 200) sampleTexts.push(m.text);
129
+
130
+ const line = `[${m.role === "user" ? "U" : "A"}${m.id}] ${m.text}\n`;
131
+
132
+ if (size + line.length > maxChars && chunk.length > 0) {
133
+ chunks.push(chunk);
134
+ if (chunks.length >= maxChunks) break;
135
+
136
+ chunk = chunk.slice(Math.max(0, chunk.length - overlapMsgs));
137
+ size = chunk.reduce((acc, mm) => acc + (`[${mm.role === "user" ? "U" : "A"}${mm.id}] ${mm.text}\n`).length, 0);
138
+ }
139
+
140
+ chunk.push(m);
141
+ size += line.length;
142
+ }
143
+
144
+ if (chunk.length > 0 && chunks.length < maxChunks) chunks.push(chunk);
145
+
146
+ const lang = detectLang(sampleTexts.join("\n"));
147
+ const messageCount = chunk.length === 0 && chunks.length === 0 ? 0 : chunks[chunks.length - 1][chunks[chunks.length - 1].length - 1].id;
148
+
149
+ return { chunks, lang, messageCount };
150
+ }
151
+
152
+ function buildMapPrompt({ lang, chunk }) {
153
+ const langInstr = lang === "zh" ? "请用中文输出 lessons。" : lang === "en" ? "Output lessons in English." : "Follow the dominant language of the transcript.";
154
+
155
+ return `You are extracting high-signal technical lessons from a chat transcript chunk.\n\nRules:\n- Output STRICT JSON only. No markdown, no backticks.\n- If nothing valuable, output: {\"lessons\":[]}\n- Max 8 lessons.\n- Each lesson.text must be <= 480 characters.\n- Categories: fact | decision | preference | other (use fact/decision primarily).\n- importance: number 0..1 (high-signal: 0.8-0.95).\n- evidence MUST quote exact short snippets from the chunk and include message_ids.\n- Do NOT include secrets/tokens/credentials.\n- Add Keywords (zh) inside each lesson:\n - Include >=1 Entity keyword that appears verbatim in the chunk (project/library/tool/service/config key/error code).\n - Include >=1 Action keyword (e.g., 修复/回滚/重启/迁移/去重/限流).\n - Include >=1 Symptom keyword (e.g., OOM/超时/429/重复/命中率差).\n - Do NOT invent entity names; copy entity keywords from the chunk.\n\n${langInstr}\n\nChunk:\n${chunk.map((m) => `[${m.role === "user" ? "U" : "A"}${m.id}] ${m.text}`).join("\n\n")}\n\nReturn JSON schema:\n{\n \"lessons\": [\n {\n \"category\": \"fact\",\n \"importance\": 0.8,\n \"text\": \"Pitfall: ... Cause: ... Fix: ... Prevention: ...\",\n \"evidence\": [\n {\"message_ids\":[12,13],\"quote\":\"...\"}\n ],\n \"tags\": [\"optional\"]\n }\n ]\n}`;
156
+ }
157
+
158
+ async function geminiGenerateJson(prompt) {
159
+ if (!GEMINI_API_KEY) throw new Error("GEMINI_API_KEY is not set");
160
+
161
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent?key=${GEMINI_API_KEY}`;
162
+
163
+ const body = {
164
+ contents: [{ role: "user", parts: [{ text: prompt }] }],
165
+ generationConfig: {
166
+ temperature: 0.2,
167
+ maxOutputTokens: 4096,
168
+ },
169
+ };
170
+
171
+ const res = await fetch(url, {
172
+ method: "POST",
173
+ headers: { "Content-Type": "application/json" },
174
+ body: JSON.stringify(body),
175
+ });
176
+
177
+ const json = await res.json();
178
+ if (!res.ok) {
179
+ throw new Error(`Gemini error ${res.status}: ${JSON.stringify(json).slice(0, 500)}`);
180
+ }
181
+
182
+ const text = json?.candidates?.[0]?.content?.parts?.map((p) => p.text).join("") || "";
183
+ return text;
184
+ }
185
+
186
+ function coerceLessons(obj) {
187
+ const lessons = Array.isArray(obj?.lessons) ? obj.lessons : [];
188
+ return lessons
189
+ .filter((l) => l && typeof l.text === "string" && l.text.trim().length >= 10)
190
+ .map((l) => ({
191
+ category: ["fact", "decision", "preference", "other"].includes(l.category) ? l.category : "other",
192
+ importance: typeof l.importance === "number" ? l.importance : 0.7,
193
+ text: l.text.trim().slice(0, 480),
194
+ evidence: Array.isArray(l.evidence) ? l.evidence : [],
195
+ tags: Array.isArray(l.tags) ? l.tags : [],
196
+ }));
197
+ }
198
+
199
+ function scoreLesson(l) {
200
+ let s = 0;
201
+ const t = l.text || "";
202
+ if (/pitfall\s*:|cause\s*:|fix\s*:|prevention\s*:/i.test(t)) s += 2;
203
+ if (/decision principle|trigger\s*:|action\s*:/i.test(t)) s += 2;
204
+ if (/\b(openclaw|docker|systemd|ssh|git|api|json|yaml|config)\b/i.test(t)) s += 1;
205
+ if (t.length < 120) s += 0.5;
206
+ if (l.evidence?.length >= 1) s += 1;
207
+ if (l.evidence?.length >= 2) s += 0.5;
208
+ const imp = Math.max(0, Math.min(1, l.importance ?? 0.7));
209
+ s += imp;
210
+ return s;
211
+ }
212
+
213
+ function reduceLessons(allLessons, maxFinal = 20) {
214
+ const seen = new Set();
215
+ const merged = [];
216
+
217
+ for (const l of allLessons) {
218
+ const key = normalizeText(l.text);
219
+ if (!key) continue;
220
+ if (seen.has(key)) continue;
221
+ seen.add(key);
222
+ merged.push(l);
223
+ }
224
+
225
+ merged.sort((a, b) => scoreLesson(b) - scoreLesson(a));
226
+
227
+ const filtered = merged.filter((l) => {
228
+ if (!l.evidence || l.evidence.length === 0) return false;
229
+ const t = normalizeText(l.text);
230
+ if (t.length < 20) return false;
231
+ if (/(be careful|best practice|should|建议|注意)/.test(t) && !/(cause|fix|prevention|trigger|action|原因|修复|预防|触发)/.test(t)) {
232
+ return false;
233
+ }
234
+ return true;
235
+ });
236
+
237
+ return filtered.slice(0, maxFinal);
238
+ }
239
+
240
+ async function importToLanceDb({ lessons, scope }) {
241
+ const importFile = path.join("/tmp", `lesson-import-${Date.now()}.json`);
242
+ const payload = {
243
+ memories: lessons.map((l) => ({
244
+ text: l.text,
245
+ importance: Math.max(0.0, Math.min(1.0, l.importance ?? 0.7)),
246
+ category: l.category,
247
+ })),
248
+ };
249
+ await fsp.writeFile(importFile, JSON.stringify(payload), "utf-8");
250
+
251
+ const { code, out, err } = await run("openclaw", ["memory-pro", "import", importFile, "--scope", scope], { cwd: REPO_ROOT });
252
+ await fsp.unlink(importFile).catch(() => {});
253
+
254
+ return { code, out, err };
255
+ }
256
+
257
+ async function notifyTelegram(chatId, message) {
258
+ const args = ["message", "send", "--channel", "telegram", "--target", String(chatId), "--message", message];
259
+ await run("openclaw", args, { cwd: REPO_ROOT });
260
+ }
261
+
262
+ async function processTaskFile(taskPath) {
263
+ const started = Date.now();
264
+ const taskRaw = await fsp.readFile(taskPath, "utf-8");
265
+ const task = JSON.parse(taskRaw);
266
+
267
+ const baseName = path.basename(taskPath);
268
+ const processingPath = path.join(PROCESSING, baseName);
269
+ await fsp.rename(taskPath, processingPath);
270
+
271
+ const result = {
272
+ taskId: task.taskId,
273
+ startedAt: nowIso(),
274
+ finishedAt: null,
275
+ ok: false,
276
+ sessionId: task.session?.sessionId,
277
+ sessionFile: task.session?.sessionFile,
278
+ stats: {},
279
+ error: null,
280
+ };
281
+
282
+ try {
283
+ const sessionFile = task.session?.sessionFile;
284
+ if (!sessionFile || !fs.existsSync(sessionFile)) {
285
+ throw new Error(`sessionFile missing or not found: ${sessionFile}`);
286
+ }
287
+
288
+ const { chunks, lang, messageCount } = await buildChunksFromJsonl(sessionFile, {
289
+ maxChars: task.extract?.mapChunkChars ?? 12000,
290
+ overlapMsgs: task.extract?.mapOverlapMsgs ?? 10,
291
+ maxChunks: 200,
292
+ });
293
+
294
+ const allLessons = [];
295
+ let mapErrors = 0;
296
+
297
+ for (let idx = 0; idx < chunks.length; idx++) {
298
+ const prompt = buildMapPrompt({ lang, chunk: chunks[idx] });
299
+ try {
300
+ const text = await geminiGenerateJson(prompt);
301
+ const obj = safeJsonParse(text);
302
+ if (!obj) {
303
+ mapErrors++;
304
+ continue;
305
+ }
306
+ const lessons = coerceLessons(obj);
307
+ for (const l of lessons) allLessons.push(l);
308
+ } catch {
309
+ mapErrors++;
310
+ }
311
+ }
312
+
313
+ const reduced = reduceLessons(allLessons, task.extract?.maxFinal ?? 20);
314
+
315
+ const scope = task.scope || "agent:main";
316
+ const importRes = await importToLanceDb({ lessons: reduced, scope });
317
+
318
+ const durationMs = Date.now() - started;
319
+ result.ok = importRes.code === 0;
320
+ result.finishedAt = nowIso();
321
+ result.stats = {
322
+ lang,
323
+ messages: messageCount,
324
+ chunks: chunks.length,
325
+ mapCandidates: allLessons.length,
326
+ mapErrors,
327
+ reduced: reduced.length,
328
+ importCode: importRes.code,
329
+ durationMs,
330
+ };
331
+
332
+ const notifyChatId = task.notify?.telegramChatId;
333
+ if (notifyChatId) {
334
+ const text = [
335
+ `Lesson Extract ✅ (${task.agentId || "main"})`,
336
+ `taskId: ${task.taskId?.slice(0, 8) || "unknown"}`,
337
+ `sessionId: ${task.session?.sessionId || "unknown"}`,
338
+ `lang: ${lang}`,
339
+ `messages: ${messageCount}, chunks: ${chunks.length}`,
340
+ `candidates: ${allLessons.length}, reduced: ${reduced.length}`,
341
+ `import: code=${importRes.code}`,
342
+ `time: ${(durationMs / 1000).toFixed(1)}s`,
343
+ ].join("\n");
344
+ await notifyTelegram(notifyChatId, text);
345
+ }
346
+
347
+ const donePath = path.join(DONE, `${task.taskId}.json`);
348
+ await fsp.writeFile(donePath, JSON.stringify(result, null, 2) + "\n", "utf-8");
349
+ await fsp.unlink(processingPath).catch(() => {});
350
+ } catch (err) {
351
+ result.ok = false;
352
+ result.finishedAt = nowIso();
353
+ result.error = err instanceof Error ? err.message : String(err);
354
+
355
+ const durationMs = Date.now() - started;
356
+ result.stats.durationMs = durationMs;
357
+
358
+ const notifyChatId = task.notify?.telegramChatId;
359
+ if (notifyChatId) {
360
+ await notifyTelegram(
361
+ notifyChatId,
362
+ `Lesson Extract ❌ (${task.agentId || "main"})\n` +
363
+ `taskId: ${task.taskId?.slice(0, 8) || "unknown"}\n` +
364
+ `error: ${result.error}\n` +
365
+ `time: ${(durationMs / 1000).toFixed(1)}s`
366
+ );
367
+ }
368
+
369
+ const errPath = path.join(ERROR, `${task.taskId}.json`);
370
+ await fsp.writeFile(errPath, JSON.stringify(result, null, 2) + "\n", "utf-8");
371
+ await fsp.unlink(processingPath).catch(() => {});
372
+ }
373
+ }
374
+
375
+ async function drainInboxOnce() {
376
+ ensureDirs();
377
+ const files = (await fsp.readdir(INBOX)).filter((f) => f.endsWith(".json")).sort();
378
+ for (const f of files) {
379
+ await processTaskFile(path.join(INBOX, f));
380
+ }
381
+ }
382
+
383
+ async function main() {
384
+ ensureDirs();
385
+ await drainInboxOnce();
386
+ if (ONCE) return;
387
+
388
+ const watcher = fs.watch(INBOX, async (_eventType, filename) => {
389
+ if (!filename || !filename.endsWith(".json")) return;
390
+ const full = path.join(INBOX, filename);
391
+ setTimeout(() => {
392
+ processTaskFile(full).catch(() => {});
393
+ }, 150);
394
+ });
395
+
396
+ let alive = true;
397
+ const shutdown = () => {
398
+ alive = false;
399
+ watcher.close();
400
+ };
401
+ process.on("SIGINT", shutdown);
402
+ process.on("SIGTERM", shutdown);
403
+
404
+ while (alive) {
405
+ await new Promise((r) => setTimeout(r, 5_000));
406
+ }
407
+ }
408
+
409
+ main().catch((err) => {
410
+ console.error(String(err));
411
+ process.exit(1);
412
+ });
@@ -0,0 +1,20 @@
1
+ [Unit]
2
+ Description=OpenClaw Lesson Extract Worker (Gemini Map-Reduce -> LanceDB Pro)
3
+ After=network-online.target
4
+ Wants=network-online.target
5
+
6
+ [Service]
7
+ Type=simple
8
+ WorkingDirectory=/home/ubuntu/clawd
9
+ EnvironmentFile=%h/.config/lesson-extract-worker.env
10
+ Environment=NODE_ENV=production
11
+ Environment=LESSON_QUEUE_ROOT=/home/ubuntu/clawd/workspaces/main/tasks/lesson-extract
12
+ Environment=GEMINI_MODEL=gemini-3-flash-preview
13
+ ExecStart=/usr/bin/env node /home/ubuntu/clawd/scripts/lesson-extract-worker.mjs
14
+ Restart=always
15
+ RestartSec=2
16
+ NoNewPrivileges=true
17
+ PrivateTmp=true
18
+
19
+ [Install]
20
+ WantedBy=default.target
package/index.ts CHANGED
@@ -639,32 +639,53 @@ const memoryLanceDBProPlugin = {
639
639
  api.registerService({
640
640
  id: "memory-lancedb-pro",
641
641
  start: async () => {
642
- try {
643
- // Test components
644
- const embedTest = await embedder.test();
645
- const retrievalTest = await retriever.test();
646
-
647
- api.logger.info(
648
- `memory-lancedb-pro: initialized successfully ` +
649
- `(embedding: ${embedTest.success ? 'OK' : 'FAIL'}, ` +
650
- `retrieval: ${retrievalTest.success ? 'OK' : 'FAIL'}, ` +
651
- `mode: ${retrievalTest.mode}, ` +
652
- `FTS: ${retrievalTest.hasFtsSupport ? 'enabled' : 'disabled'})`
653
- );
654
-
655
- if (!embedTest.success) {
656
- api.logger.warn(`memory-lancedb-pro: embedding test failed: ${embedTest.error}`);
642
+ // IMPORTANT: Do not block gateway startup on external network calls.
643
+ // If embedding/retrieval tests hang (bad network / slow provider), the gateway
644
+ // may never bind its HTTP port, causing restart timeouts.
645
+
646
+ const withTimeout = async <T>(p: Promise<T>, ms: number, label: string): Promise<T> => {
647
+ let timeout: ReturnType<typeof setTimeout> | undefined;
648
+ const timeoutPromise = new Promise<never>((_, reject) => {
649
+ timeout = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
650
+ });
651
+ try {
652
+ return await Promise.race([p, timeoutPromise]);
653
+ } finally {
654
+ if (timeout) clearTimeout(timeout);
657
655
  }
658
- if (!retrievalTest.success) {
659
- api.logger.warn(`memory-lancedb-pro: retrieval test failed: ${retrievalTest.error}`);
656
+ };
657
+
658
+ const runStartupChecks = async () => {
659
+ try {
660
+ // Test components (bounded time)
661
+ const embedTest = await withTimeout(embedder.test(), 8_000, "embedder.test()");
662
+ const retrievalTest = await withTimeout(retriever.test(), 8_000, "retriever.test()");
663
+
664
+ api.logger.info(
665
+ `memory-lancedb-pro: initialized successfully ` +
666
+ `(embedding: ${embedTest.success ? "OK" : "FAIL"}, ` +
667
+ `retrieval: ${retrievalTest.success ? "OK" : "FAIL"}, ` +
668
+ `mode: ${retrievalTest.mode}, ` +
669
+ `FTS: ${retrievalTest.hasFtsSupport ? "enabled" : "disabled"})`
670
+ );
671
+
672
+ if (!embedTest.success) {
673
+ api.logger.warn(`memory-lancedb-pro: embedding test failed: ${embedTest.error}`);
674
+ }
675
+ if (!retrievalTest.success) {
676
+ api.logger.warn(`memory-lancedb-pro: retrieval test failed: ${retrievalTest.error}`);
677
+ }
678
+ } catch (error) {
679
+ api.logger.warn(`memory-lancedb-pro: startup checks failed: ${String(error)}`);
660
680
  }
681
+ };
661
682
 
662
- // Run initial backup after a short delay, then schedule daily
663
- setTimeout(() => runBackup(), 60_000); // 1 min after start
664
- backupTimer = setInterval(() => runBackup(), BACKUP_INTERVAL_MS);
665
- } catch (error) {
666
- api.logger.warn(`memory-lancedb-pro: startup test failed: ${String(error)}`);
667
- }
683
+ // Fire-and-forget: allow gateway to start serving immediately.
684
+ setTimeout(() => void runStartupChecks(), 0);
685
+
686
+ // Run initial backup after a short delay, then schedule daily
687
+ setTimeout(() => void runBackup(), 60_000); // 1 min after start
688
+ backupTimer = setInterval(() => void runBackup(), BACKUP_INTERVAL_MS);
668
689
  },
669
690
  stop: () => {
670
691
  if (backupTimer) {
@@ -2,7 +2,7 @@
2
2
  "id": "memory-lancedb-pro",
3
3
  "name": "Memory (LanceDB Pro)",
4
4
  "description": "Enhanced LanceDB-backed long-term memory with hybrid retrieval, multi-scope isolation, and management CLI",
5
- "version": "1.0.7",
5
+ "version": "1.0.10",
6
6
  "kind": "memory",
7
7
  "configSchema": {
8
8
  "type": "object",
@@ -108,7 +108,7 @@
108
108
  },
109
109
  "rerankModel": {
110
110
  "type": "string",
111
- "default": "jina-reranker-v2-base-multilingual",
111
+ "default": "jina-reranker-v3",
112
112
  "description": "Reranker model name"
113
113
  },
114
114
  "rerankEndpoint": {
@@ -118,7 +118,11 @@
118
118
  },
119
119
  "rerankProvider": {
120
120
  "type": "string",
121
- "enum": ["jina", "siliconflow", "pinecone"],
121
+ "enum": [
122
+ "jina",
123
+ "siliconflow",
124
+ "pinecone"
125
+ ],
122
126
  "default": "jina",
123
127
  "description": "Reranker provider format. Determines request/response shape and auth header."
124
128
  },
@@ -300,8 +304,8 @@
300
304
  },
301
305
  "retrieval.rerankModel": {
302
306
  "label": "Reranker Model",
303
- "placeholder": "jina-reranker-v2-base-multilingual",
304
- "help": "Reranker model name (e.g. jina-reranker-v2-base-multilingual, BAAI/bge-reranker-v2-m3)",
307
+ "placeholder": "jina-reranker-v3",
308
+ "help": "Reranker model name (e.g. jina-reranker-v3, BAAI/bge-reranker-v2-m3)",
305
309
  "advanced": true
306
310
  },
307
311
  "retrieval.rerankEndpoint": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-lancedb-pro",
3
- "version": "1.0.7",
3
+ "version": "1.0.10",
4
4
  "description": "OpenClaw enhanced LanceDB memory plugin with hybrid retrieval (Vector + BM25), cross-encoder rerank, multi-scope isolation, and management CLI",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -36,6 +36,22 @@ EXCLUDED_AGENT_IDS = {
36
36
  "memory-distiller",
37
37
  }
38
38
 
39
+ # Source allowlist (optional quality control).
40
+ # Default (env unset): allow all agents (except EXCLUDED_AGENT_IDS).
41
+ # If set: only distill from the listed agent IDs.
42
+ # Example:
43
+ # OPENCLAW_JSONL_DISTILL_ALLOWED_AGENT_IDS=main,code-agent
44
+ ENV_ALLOWED_AGENT_IDS = "OPENCLAW_JSONL_DISTILL_ALLOWED_AGENT_IDS"
45
+
46
+
47
+ def _get_allowed_agent_ids() -> Optional[set[str]]:
48
+ raw = os.environ.get(ENV_ALLOWED_AGENT_IDS, "").strip()
49
+ if not raw or raw in ("*", "all"):
50
+ return None
51
+ parts = [p.strip() for p in raw.split(",") if p.strip()]
52
+ return set(parts) if parts else None
53
+
54
+
39
55
 
40
56
  NOISE_PREFIXES = (
41
57
  "✅ New session started",
@@ -175,12 +191,16 @@ def _list_session_files(agents_dir: Path) -> List[Tuple[str, Path]]:
175
191
  if not agents_dir.exists():
176
192
  return results
177
193
 
194
+ allowed_agent_ids = _get_allowed_agent_ids()
195
+
178
196
  for agent_dir in sorted(agents_dir.iterdir()):
179
197
  if not agent_dir.is_dir():
180
198
  continue
181
199
  agent_id = agent_dir.name
182
200
  if agent_id in EXCLUDED_AGENT_IDS:
183
201
  continue
202
+ if allowed_agent_ids is not None and agent_id not in allowed_agent_ids:
203
+ continue
184
204
  sessions_dir = agent_dir / "sessions"
185
205
  if not sessions_dir.exists():
186
206
  continue
package/src/retriever.ts CHANGED
@@ -26,7 +26,7 @@ export interface RetrievalConfig {
26
26
  filterNoise: boolean;
27
27
  /** Reranker API key (enables cross-encoder reranking) */
28
28
  rerankApiKey?: string;
29
- /** Reranker model (default: jina-reranker-v2-base-multilingual) */
29
+ /** Reranker model (default: jina-reranker-v3) */
30
30
  rerankModel?: string;
31
31
  /** Reranker API endpoint (default: https://api.jina.ai/v1/rerank). */
32
32
  rerankEndpoint?: string;
@@ -90,7 +90,7 @@ export const DEFAULT_RETRIEVAL_CONFIG: RetrievalConfig = {
90
90
  recencyHalfLifeDays: 14,
91
91
  recencyWeight: 0.10,
92
92
  filterNoise: true,
93
- rerankModel: "jina-reranker-v2-base-multilingual",
93
+ rerankModel: "jina-reranker-v3",
94
94
  rerankEndpoint: "https://api.jina.ai/v1/rerank",
95
95
  lengthNormAnchor: 500,
96
96
  hardMinScore: 0.35,
@@ -427,7 +427,7 @@ export class MemoryRetriever {
427
427
  if (this.config.rerank === "cross-encoder" && this.config.rerankApiKey) {
428
428
  try {
429
429
  const provider = this.config.rerankProvider || "jina";
430
- const model = this.config.rerankModel || "jina-reranker-v2-base-multilingual";
430
+ const model = this.config.rerankModel || "jina-reranker-v3";
431
431
  const endpoint = this.config.rerankEndpoint || "https://api.jina.ai/v1/rerank";
432
432
  const documents = results.map(r => r.entry.text);
433
433