From 7cb7b8f0dc0c36009f43c049b6a099eb851d3523 Mon Sep 17 00:00:00 2001 From: TriDefender Date: Tue, 4 Aug 2026 23:37:11 +0800 Subject: [PATCH 1/8] Remove commit message for embedder changes --- commit_msg.txt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/commit_msg.txt b/commit_msg.txt index 682aec2c8..8b1378917 100644 --- a/commit_msg.txt +++ b/commit_msg.txt @@ -1,7 +1 @@ -fix(embedder): address PR review comments (Issue #629) -- Add embedder-ollama-batch-routing.test.mjs to CI manifest -- Add comments explaining why provider options are omitted for Ollama batch -- Add note about /v1/embeddings no-fallback assumption - -Reviewed by: rwmjhb \ No newline at end of file From d28c751918cf097609827b38bb5a5a78ff2e6dde Mon Sep 17 00:00:00 2001 From: TriDefender Date: Tue, 4 Aug 2026 23:37:11 +0800 Subject: [PATCH 2/8] Remove commit message for embedder changes Revert "Remove commit message for embedder changes" This reverts commit 7cb7b8f0dc0c36009f43c049b6a099eb851d3523. Remove commit message for embedder changes Introduced in commit 3697ed5, this is a random commit message left by some coding agent and does nothing --- commit_msg.txt | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 commit_msg.txt diff --git a/commit_msg.txt b/commit_msg.txt deleted file mode 100644 index 682aec2c8..000000000 --- a/commit_msg.txt +++ /dev/null @@ -1,7 +0,0 @@ -fix(embedder): address PR review comments (Issue #629) - -- Add embedder-ollama-batch-routing.test.mjs to CI manifest -- Add comments explaining why provider options are omitted for Ollama batch -- Add note about /v1/embeddings no-fallback assumption - -Reviewed by: rwmjhb \ No newline at end of file From 658d513bad4836e8740540bdff51bcce1f89a749 Mon Sep 17 00:00:00 2001 From: TriDefender Date: Wed, 5 Aug 2026 00:00:25 +0800 Subject: [PATCH 3/8] Removed src/admission-stats.ts Ported to master by commit 9c69ea4, src/admission-stats.ts is dead on arrival, nothing references this. It's even not covered by CI --- src/admission-stats.ts | 332 ----------------------------------------- 1 file changed, 332 deletions(-) delete mode 100644 src/admission-stats.ts diff --git a/src/admission-stats.ts b/src/admission-stats.ts deleted file mode 100644 index 1cd076208..000000000 --- a/src/admission-stats.ts +++ /dev/null @@ -1,332 +0,0 @@ -import { readFile } from "node:fs/promises"; -import type { AdmissionControlConfig, AdmissionRejectionAuditEntry } from "./admission-control.js"; -import { resolveRejectedAuditFilePath } from "./admission-control.js"; -import { parseSmartMetadata } from "./smart-metadata.js"; - -const DEFAULT_TOP_REJECTION_REASONS = 5; -const ADMISSION_WINDOWS = [ - { key: "last24h", durationMs: 24 * 60 * 60 * 1000 }, - { key: "last7d", durationMs: 7 * 24 * 60 * 60 * 1000 }, -] as const; - -export interface AdmissionAuditedMemoryLike { - metadata?: string; - timestamp?: number; - category?: string; - text?: string; - importance?: number; -} - -export interface AdmissionStatsStoreLike { - dbPath: string; - list?: ( - scopeFilter?: string[], - category?: string, - limit?: number, - offset?: number, - ) => Promise; -} - -export interface AdmissionCategoryBreakdown { - admittedCount: number | null; - rejectedCount: number; - totalObserved: number | null; - rejectRate: number | null; -} - -export interface AdmissionWindowBreakdown { - admittedCount: number | null; - rejectedCount: number; - totalObserved: number | null; - rejectRate: number | null; -} - -export interface AdmissionRejectionReasonCount { - label: string; - count: number; -} - -export interface AdmissionRejectionSummary { - total: number; - latestRejectedAt: number | null; - byCategory: Record; - byScope: Record; - topReasons: AdmissionRejectionReasonCount[]; -} - -export interface AdmissionStatsSummary { - enabled: boolean; - auditMetadataEnabled: boolean; - rejectedAuditFilePath: string; - rejectedCount: number; - admittedCount: number | null; - totalObserved: number | null; - rejectRate: number | null; - latestRejectedAt: number | null; - rejectedByCategory: Record; - rejectedByScope: Record; - categoryBreakdown: Record; - topReasons: AdmissionRejectionReasonCount[]; - windows: Record; - observedAuditedMemories: number; -} - -export async function readAdmissionRejectionAudits( - filePath: string, -): Promise { - try { - const raw = await readFile(filePath, "utf8"); - const entries: AdmissionRejectionAuditEntry[] = []; - for (const rawLine of raw.split(/\r?\n/)) { - const line = rawLine.trim(); - if (!line) continue; - try { - entries.push(JSON.parse(line) as AdmissionRejectionAuditEntry); - } catch { - // Skip corrupt JSONL lines (truncated writes, disk errors, etc.) - } - } - return entries; - } catch (error) { - const err = error as NodeJS.ErrnoException; - if (err?.code === "ENOENT") { - return []; - } - throw error; - } -} - -export function normalizeReasonKey(reason: string): string { - return reason - .toLowerCase() - .replace(/\d+(?:\.\d+)?/g, "#") - .replace(/\s+/g, " ") - .trim(); -} - -export function extractAdmissionReasonLabel(entry: AdmissionRejectionAuditEntry): string { - const utilityReason = entry.audit.utility_reason?.trim(); - if (utilityReason) { - return utilityReason; - } - return entry.audit.reason.trim(); -} - -export function summarizeAdmissionRejections( - entries: AdmissionRejectionAuditEntry[], -): AdmissionRejectionSummary { - const byCategory: Record = {}; - const byScope: Record = {}; - const reasonCounts = new Map(); - - for (const entry of entries) { - byCategory[entry.candidate.category] = (byCategory[entry.candidate.category] ?? 0) + 1; - byScope[entry.target_scope] = (byScope[entry.target_scope] ?? 0) + 1; - const label = extractAdmissionReasonLabel(entry); - const key = normalizeReasonKey(label); - const current = reasonCounts.get(key); - if (current) { - current.count += 1; - } else { - reasonCounts.set(key, { label, count: 1 }); - } - } - - const latestRejectedAt = entries.length > 0 - ? Math.max(...entries.map((entry) => entry.rejected_at)) - : null; - const topReasons = Array.from(reasonCounts.values()) - .sort((left, right) => right.count - left.count || left.label.localeCompare(right.label)) - .slice(0, DEFAULT_TOP_REJECTION_REASONS); - - return { - total: entries.length, - latestRejectedAt, - byCategory, - byScope, - topReasons, - }; -} - -export function getAdmissionAuditDecision( - entry: { metadata?: string }, -): "pass_to_dedup" | "reject" | null { - try { - const parsed = JSON.parse(entry.metadata || "{}") as Record; - const audit = parsed.admission_control as Record | undefined; - const decision = audit?.decision; - return decision === "pass_to_dedup" || decision === "reject" ? decision : null; - } catch { - return null; - } -} - -export function getAdmittedDecisionTimestamp( - entry: { metadata?: string; timestamp?: number }, -): number | null { - try { - const parsed = JSON.parse(entry.metadata || "{}") as Record; - const audit = parsed.admission_control as Record | undefined; - const evaluatedAt = Number(audit?.evaluated_at); - if (Number.isFinite(evaluatedAt) && evaluatedAt > 0) { - return evaluatedAt; - } - } catch { - // ignore - } - - const timestamp = Number(entry.timestamp); - if (Number.isFinite(timestamp) && timestamp > 0) { - return timestamp; - } - return null; -} - -export function getObservedAdmissionCategory( - entry: AdmissionAuditedMemoryLike, -): string { - return parseSmartMetadata(entry.metadata, entry as any).memory_category || entry.category || "patterns"; -} - -export function buildAdmissionCategoryBreakdown( - admittedCategories: string[] | null, - rejectedEntries: AdmissionRejectionAuditEntry[], -): Record { - const admittedCounts: Record | null = admittedCategories ? {} : null; - const rejectedCounts: Record = {}; - - if (admittedCategories) { - for (const category of admittedCategories) { - admittedCounts[category] = (admittedCounts[category] ?? 0) + 1; - } - } - - for (const entry of rejectedEntries) { - const category = entry.candidate.category; - rejectedCounts[category] = (rejectedCounts[category] ?? 0) + 1; - } - - const categories = Array.from( - new Set([ - ...Object.keys(rejectedCounts), - ...(admittedCounts ? Object.keys(admittedCounts) : []), - ]), - ).sort((left, right) => left.localeCompare(right)); - - const breakdown: Record = {}; - for (const category of categories) { - const admittedCount = admittedCounts ? (admittedCounts[category] ?? 0) : null; - const rejectedCount = rejectedCounts[category] ?? 0; - const totalObserved = admittedCount !== null ? admittedCount + rejectedCount : null; - const rejectRate = - totalObserved && totalObserved > 0 ? rejectedCount / totalObserved : null; - - breakdown[category] = { - admittedCount, - rejectedCount, - totalObserved, - rejectRate, - }; - } - - return breakdown; -} - -export function buildAdmissionWindowSummary( - admittedTimestamps: number[] | null, - rejectedEntries: AdmissionRejectionAuditEntry[], - now = Date.now(), -): Record { - const windows: Record = {}; - - for (const windowDef of ADMISSION_WINDOWS) { - const since = now - windowDef.durationMs; - const rejectedCount = rejectedEntries.filter((entry) => entry.rejected_at >= since).length; - const admittedCount = admittedTimestamps - ? admittedTimestamps.filter((ts) => ts >= since).length - : null; - const totalObserved = admittedCount !== null ? admittedCount + rejectedCount : null; - const rejectRate = - totalObserved && totalObserved > 0 ? rejectedCount / totalObserved : null; - - windows[windowDef.key] = { - admittedCount, - rejectedCount, - totalObserved, - rejectRate, - }; - } - - return windows; -} - -export async function buildAdmissionStats(params: { - store: AdmissionStatsStoreLike; - admissionControl?: AdmissionControlConfig; - scopeFilter?: string[]; - memoryTotalCount: number; -}): Promise { - const rejectionFilePath = resolveRejectedAuditFilePath( - params.store.dbPath, - params.admissionControl, - ); - let rejectionEntries = await readAdmissionRejectionAudits(rejectionFilePath); - if (params.scopeFilter && params.scopeFilter.length > 0) { - const scopeSet = new Set(params.scopeFilter); - rejectionEntries = rejectionEntries.filter((entry) => scopeSet.has(entry.target_scope)); - } - - const rejectionSummary = summarizeAdmissionRejections(rejectionEntries); - const auditMetadataEnabled = params.admissionControl?.auditMetadata !== false; - let admittedCount: number | null = null; - let admittedTimestamps: number[] | null = null; - let admittedCategories: string[] | null = null; - let observedAuditedMemories = 0; - - if (auditMetadataEnabled && typeof params.store.list === "function") { - const memories = await params.store.list( - params.scopeFilter, - undefined, - Math.max(params.memoryTotalCount, 1), - 0, - ); - admittedCount = 0; - admittedTimestamps = []; - admittedCategories = []; - for (const memory of memories) { - const decision = getAdmissionAuditDecision(memory); - if (decision === "pass_to_dedup") { - admittedCount += 1; - observedAuditedMemories += 1; - admittedCategories.push(getObservedAdmissionCategory(memory)); - const admittedAt = getAdmittedDecisionTimestamp(memory); - if (admittedAt !== null) { - admittedTimestamps.push(admittedAt); - } - } else if (decision === "reject") { - observedAuditedMemories += 1; - } - } - } - - const totalObserved = admittedCount !== null ? admittedCount + rejectionSummary.total : null; - const rejectRate = - totalObserved && totalObserved > 0 ? rejectionSummary.total / totalObserved : null; - - return { - enabled: params.admissionControl?.enabled === true, - auditMetadataEnabled, - rejectedAuditFilePath: rejectionFilePath, - rejectedCount: rejectionSummary.total, - admittedCount, - totalObserved, - rejectRate, - latestRejectedAt: rejectionSummary.latestRejectedAt, - rejectedByCategory: rejectionSummary.byCategory, - rejectedByScope: rejectionSummary.byScope, - categoryBreakdown: buildAdmissionCategoryBreakdown(admittedCategories, rejectionEntries), - topReasons: rejectionSummary.topReasons, - windows: buildAdmissionWindowSummary(admittedTimestamps, rejectionEntries), - observedAuditedMemories, - }; -} From 2aa51090b9a9113b2d9f13e3e3d8b0b4ea6c6e4f Mon Sep 17 00:00:00 2001 From: TriDefender Date: Wed, 5 Aug 2026 00:01:25 +0800 Subject: [PATCH 4/8] Delete restore_files.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Not referenced by any script/package/docs. Uses git show upstream_master: — a ref that doesn't exist in this repo (verified: no local/remote upstream_master). Introduced 2026-05-15 (38eba06). Orphaned since birth, probably some helper script that didn't get pruned out of a commit. --- restore_files.py | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 restore_files.py diff --git a/restore_files.py b/restore_files.py deleted file mode 100644 index e0f99d5f8..000000000 --- a/restore_files.py +++ /dev/null @@ -1,14 +0,0 @@ -import subprocess - -files = [ - ('upstream_master:index.ts', 'index.ts'), - ('upstream_master:src/chunker.ts', 'src/chunker.ts'), - ('upstream_master:package.json', 'package.json'), - ('upstream_master:package-lock.json', 'package-lock.json'), -] - -for src, dst in files: - content = subprocess.check_output(['git', 'show', src], text=True, encoding='utf-8', errors='replace') - with open(dst, 'w', encoding='utf-8') as f: - f.write(content) - print(f'Restored {dst} ({len(content)} chars)') \ No newline at end of file From 41e2b425a1b82fd938770f5aa8dc15155072caa1 Mon Sep 17 00:00:00 2001 From: TriDefender Date: Sun, 9 Aug 2026 17:08:27 +0800 Subject: [PATCH 5/8] Delete admission-stats.js --- dist/src/admission-stats.js | 213 ------------------------------------ 1 file changed, 213 deletions(-) delete mode 100644 dist/src/admission-stats.js diff --git a/dist/src/admission-stats.js b/dist/src/admission-stats.js deleted file mode 100644 index f8c3da941..000000000 --- a/dist/src/admission-stats.js +++ /dev/null @@ -1,213 +0,0 @@ -import { readFile } from "node:fs/promises"; -import { resolveRejectedAuditFilePath } from "./admission-control.js"; -import { parseSmartMetadata } from "./smart-metadata.js"; -const DEFAULT_TOP_REJECTION_REASONS = 5; -const ADMISSION_WINDOWS = [ - { key: "last24h", durationMs: 24 * 60 * 60 * 1000 }, - { key: "last7d", durationMs: 7 * 24 * 60 * 60 * 1000 }, -]; -export async function readAdmissionRejectionAudits(filePath) { - try { - const raw = await readFile(filePath, "utf8"); - const entries = []; - for (const rawLine of raw.split(/\r?\n/)) { - const line = rawLine.trim(); - if (!line) - continue; - try { - entries.push(JSON.parse(line)); - } - catch { - // Skip corrupt JSONL lines (truncated writes, disk errors, etc.) - } - } - return entries; - } - catch (error) { - const err = error; - if (err?.code === "ENOENT") { - return []; - } - throw error; - } -} -export function normalizeReasonKey(reason) { - return reason - .toLowerCase() - .replace(/\d+(?:\.\d+)?/g, "#") - .replace(/\s+/g, " ") - .trim(); -} -export function extractAdmissionReasonLabel(entry) { - const utilityReason = entry.audit.utility_reason?.trim(); - if (utilityReason) { - return utilityReason; - } - return entry.audit.reason.trim(); -} -export function summarizeAdmissionRejections(entries) { - const byCategory = {}; - const byScope = {}; - const reasonCounts = new Map(); - for (const entry of entries) { - byCategory[entry.candidate.category] = (byCategory[entry.candidate.category] ?? 0) + 1; - byScope[entry.target_scope] = (byScope[entry.target_scope] ?? 0) + 1; - const label = extractAdmissionReasonLabel(entry); - const key = normalizeReasonKey(label); - const current = reasonCounts.get(key); - if (current) { - current.count += 1; - } - else { - reasonCounts.set(key, { label, count: 1 }); - } - } - const latestRejectedAt = entries.length > 0 - ? Math.max(...entries.map((entry) => entry.rejected_at)) - : null; - const topReasons = Array.from(reasonCounts.values()) - .sort((left, right) => right.count - left.count || left.label.localeCompare(right.label)) - .slice(0, DEFAULT_TOP_REJECTION_REASONS); - return { - total: entries.length, - latestRejectedAt, - byCategory, - byScope, - topReasons, - }; -} -export function getAdmissionAuditDecision(entry) { - try { - const parsed = JSON.parse(entry.metadata || "{}"); - const audit = parsed.admission_control; - const decision = audit?.decision; - return decision === "pass_to_dedup" || decision === "reject" ? decision : null; - } - catch { - return null; - } -} -export function getAdmittedDecisionTimestamp(entry) { - try { - const parsed = JSON.parse(entry.metadata || "{}"); - const audit = parsed.admission_control; - const evaluatedAt = Number(audit?.evaluated_at); - if (Number.isFinite(evaluatedAt) && evaluatedAt > 0) { - return evaluatedAt; - } - } - catch { - // ignore - } - const timestamp = Number(entry.timestamp); - if (Number.isFinite(timestamp) && timestamp > 0) { - return timestamp; - } - return null; -} -export function getObservedAdmissionCategory(entry) { - return parseSmartMetadata(entry.metadata, entry).memory_category || entry.category || "patterns"; -} -export function buildAdmissionCategoryBreakdown(admittedCategories, rejectedEntries) { - const admittedCounts = admittedCategories ? {} : null; - const rejectedCounts = {}; - if (admittedCategories) { - for (const category of admittedCategories) { - admittedCounts[category] = (admittedCounts[category] ?? 0) + 1; - } - } - for (const entry of rejectedEntries) { - const category = entry.candidate.category; - rejectedCounts[category] = (rejectedCounts[category] ?? 0) + 1; - } - const categories = Array.from(new Set([ - ...Object.keys(rejectedCounts), - ...(admittedCounts ? Object.keys(admittedCounts) : []), - ])).sort((left, right) => left.localeCompare(right)); - const breakdown = {}; - for (const category of categories) { - const admittedCount = admittedCounts ? (admittedCounts[category] ?? 0) : null; - const rejectedCount = rejectedCounts[category] ?? 0; - const totalObserved = admittedCount !== null ? admittedCount + rejectedCount : null; - const rejectRate = totalObserved && totalObserved > 0 ? rejectedCount / totalObserved : null; - breakdown[category] = { - admittedCount, - rejectedCount, - totalObserved, - rejectRate, - }; - } - return breakdown; -} -export function buildAdmissionWindowSummary(admittedTimestamps, rejectedEntries, now = Date.now()) { - const windows = {}; - for (const windowDef of ADMISSION_WINDOWS) { - const since = now - windowDef.durationMs; - const rejectedCount = rejectedEntries.filter((entry) => entry.rejected_at >= since).length; - const admittedCount = admittedTimestamps - ? admittedTimestamps.filter((ts) => ts >= since).length - : null; - const totalObserved = admittedCount !== null ? admittedCount + rejectedCount : null; - const rejectRate = totalObserved && totalObserved > 0 ? rejectedCount / totalObserved : null; - windows[windowDef.key] = { - admittedCount, - rejectedCount, - totalObserved, - rejectRate, - }; - } - return windows; -} -export async function buildAdmissionStats(params) { - const rejectionFilePath = resolveRejectedAuditFilePath(params.store.dbPath, params.admissionControl); - let rejectionEntries = await readAdmissionRejectionAudits(rejectionFilePath); - if (params.scopeFilter && params.scopeFilter.length > 0) { - const scopeSet = new Set(params.scopeFilter); - rejectionEntries = rejectionEntries.filter((entry) => scopeSet.has(entry.target_scope)); - } - const rejectionSummary = summarizeAdmissionRejections(rejectionEntries); - const auditMetadataEnabled = params.admissionControl?.auditMetadata !== false; - let admittedCount = null; - let admittedTimestamps = null; - let admittedCategories = null; - let observedAuditedMemories = 0; - if (auditMetadataEnabled && typeof params.store.list === "function") { - const memories = await params.store.list(params.scopeFilter, undefined, Math.max(params.memoryTotalCount, 1), 0); - admittedCount = 0; - admittedTimestamps = []; - admittedCategories = []; - for (const memory of memories) { - const decision = getAdmissionAuditDecision(memory); - if (decision === "pass_to_dedup") { - admittedCount += 1; - observedAuditedMemories += 1; - admittedCategories.push(getObservedAdmissionCategory(memory)); - const admittedAt = getAdmittedDecisionTimestamp(memory); - if (admittedAt !== null) { - admittedTimestamps.push(admittedAt); - } - } - else if (decision === "reject") { - observedAuditedMemories += 1; - } - } - } - const totalObserved = admittedCount !== null ? admittedCount + rejectionSummary.total : null; - const rejectRate = totalObserved && totalObserved > 0 ? rejectionSummary.total / totalObserved : null; - return { - enabled: params.admissionControl?.enabled === true, - auditMetadataEnabled, - rejectedAuditFilePath: rejectionFilePath, - rejectedCount: rejectionSummary.total, - admittedCount, - totalObserved, - rejectRate, - latestRejectedAt: rejectionSummary.latestRejectedAt, - rejectedByCategory: rejectionSummary.byCategory, - rejectedByScope: rejectionSummary.byScope, - categoryBreakdown: buildAdmissionCategoryBreakdown(admittedCategories, rejectionEntries), - topReasons: rejectionSummary.topReasons, - windows: buildAdmissionWindowSummary(admittedTimestamps, rejectionEntries), - observedAuditedMemories, - }; -} From 47d11cc2e02f2a1edfcdecbeb2a287f2e47bb7a7 Mon Sep 17 00:00:00 2001 From: TriDefender Date: Sun, 9 Aug 2026 17:09:19 +0800 Subject: [PATCH 6/8] Delete commit_msg.txt --- commit_msg.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 commit_msg.txt diff --git a/commit_msg.txt b/commit_msg.txt deleted file mode 100644 index 8b1378917..000000000 --- a/commit_msg.txt +++ /dev/null @@ -1 +0,0 @@ - From 077e4f59e6ac4ed138afdf0b0c61756be2df471a Mon Sep 17 00:00:00 2001 From: TriDefender Date: Sun, 9 Aug 2026 17:10:38 +0800 Subject: [PATCH 7/8] Delete CHANGELOG-v1.1.0.md Duplicate from file in `./docs` --- CHANGELOG-v1.1.0.md | 234 -------------------------------------------- 1 file changed, 234 deletions(-) delete mode 100644 CHANGELOG-v1.1.0.md diff --git a/CHANGELOG-v1.1.0.md b/CHANGELOG-v1.1.0.md deleted file mode 100644 index 4b233b051..000000000 --- a/CHANGELOG-v1.1.0.md +++ /dev/null @@ -1,234 +0,0 @@ -## 1.1.0-beta.11 (OpenClaw 2026.5 runtime compatibility) - -- Ship compiled `dist/index.js` runtime and point package/OpenClaw extension entries at it. -- Declare `contracts.tools` for registered agent tools. -- Avoid double-resolving already-absolute backup/admission audit paths. -- Load LanceDB via ESM dynamic `import()` instead of `require()`. - -# memory-lancedb-pro v1.1.0 — 智能记忆增强 - -> **日期**: 2026-03-03 -> **作者**: CJY -> **概述**: 基于对 AI Agent 记忆系统的深入理解,对记忆的写入质量、生命周期管理和去重能力进行了全面改进与完善 - ---- - -## 一、改进动机 - -原有记忆系统在**检索侧**表现优异(Vector+BM25 混合检索、cross-encoder 重排序、多维评分),但在以下方面存在提升空间: - -- **记忆写入质量**:依赖正则表达式触发捕获,容易漏捕有价值信息或误捕噪声 -- **记忆结构层次**:扁平文本存储,缺乏分层索引能力 -- **记忆生命周期**:简单时间衰减,无法模拟人类记忆的遗忘与强化规律 -- **去重能力**:仅基于向量相似度的粗粒度去重,缺乏语义级判断 - -本次改进针对这三个维度进行了系统性增强。 - ---- - -## 二、变更摘要 - -| 改进维度 | 核心变更 | 效果 | -| ------------ | ----------------------------------------- | ---------------------------------- | -| 智能提取 | LLM 驱动的 6 类别提取 + L0/L1/L2 分层存储 | 记忆写入更精准、结构更丰富 | -| 生命周期管理 | Weibull 衰减模型 + 三层晋升/降级 | 重要记忆持久保留,过时记忆自然淡化 | -| 智能去重 | 向量预过滤 + LLM 语义决策 | 避免冗余记忆,支持信息演化合并 | - ---- - -## 三、新增文件 - -### 1. `src/memory-categories.ts` — 6 类别分类系统 - -设计了语义明确的记忆分类体系,将记忆分为两大类六小类: - -- **用户记忆**:`profile`(身份属性)、`preferences`(偏好习惯)、`entities`(持续存在的实体)、`events`(发生的事件) -- **Agent 记忆**:`cases`(问题-解决方案对)、`patterns`(可复用的处理流程) - -每个类别有不同的合并策略: - -- `profile` → 始终合并(用户身份信息持续累积) -- `preferences` / `entities` / `patterns` → 支持智能合并 -- `events` / `cases` → 仅新增或跳过(独立记录,保留历史完整性) - ---- - -### 2. `src/llm-client.ts` — LLM 客户端 - -封装了 LLM 调用接口,专注于结构化 JSON 输出: - -- 复用现有 OpenAI SDK 依赖,零新增包 -- 内置 JSON 容错解析:支持 markdown 代码块包裹和平衡大括号提取 -- 低温度 (0.1) 保证输出一致性 -- 30 秒超时保护,失败时优雅降级 - ---- - -### 3. `src/extraction-prompts.ts` — 记忆提取提示模板 - -精心设计了 3 个提示模板: - -| 函数 | 用途 | -| ------------------------- | --------------------------------------------------- | -| `buildExtractionPrompt()` | 从对话中提取 6 类别 L0/L1/L2 记忆,含 few-shot 示例 | -| `buildDedupPrompt()` | CREATE / MERGE / SKIP 去重决策 | -| `buildMergePrompt()` | 将新旧记忆合并为三层结构 | - -提取提示包含完整的记忆价值判断标准、类别决策逻辑表、常见混淆澄清规则和 6 个 few-shot 示例。 - ---- - -### 4. `src/smart-extractor.ts` — 智能提取管线 - -实现了完整的 LLM 驱动提取流水线: - -``` -对话文本 → LLM 提取 → 候选记忆 → 向量去重 → LLM 决策 → 持久化 -``` - -核心设计: - -- **两阶段去重**:先用向量相似度(阈值 0.7)快速筛选候选,再用 LLM 进行语义级判断 -- **类别感知合并**:不同类别应用不同合并策略 -- **L0/L1/L2 三层存储**:L0 一句话索引用于检索注入,L1 结构化摘要用于精读,L2 完整叙述用于深度回顾 -- **向后兼容**:新增的 6 类别自动映射到已有的 5 类别存储,L0/L1/L2 存储在 metadata JSON 中 -- **按类别设定重要度**:profile (0.9) > patterns (0.85) > cases/preferences (0.8) > entities (0.7) > events (0.6) - ---- - -### 5. `src/decay-engine.ts` — Weibull 衰减引擎 - -基于认知心理学中的记忆遗忘曲线研究,实现了复合衰减模型: - -**复合分数 = 时效权重 × 时效 + 频率权重 × 频率 + 内在权重 × 内在价值** - -三个分量: - -| 分量 | 机制 | 含义 | -| ------------------------ | --------------------------------- | ---------------------- | -| **时效 (recency)** | Weibull 拉伸指数衰减 `exp(-λt^β)` | 越久远的记忆衰减越快 | -| **频率 (frequency)** | 对数饱和曲线 + 时间加权 | 越常被访问的记忆越活跃 | -| **内在价值 (intrinsic)** | `importance × confidence` | 高价值记忆天然抵抗遗忘 | - -层级特定的衰减形状 (β 参数): - -- **Core** (β=0.8):亚指数衰减 → 遗忘极慢,衰减地板 0.9 -- **Working** (β=1.0):标准指数衰减,衰减地板 0.7 -- **Peripheral** (β=1.3):超指数衰减 → 遗忘加速,衰减地板 0.5 - -关键特性: - -- **重要性调制半衰期**:`effectiveHL = halfLife × exp(μ × importance)`,重要记忆持续更久 -- **搜索结果加权**:检索时自动应用衰减加权,让活跃记忆排名更高 -- **过期识别**:识别 composite < 0.3 的过期记忆 - ---- - -### 6. `src/tier-manager.ts` — 三层晋升/降级管理器 - -模拟人类记忆的多级存储模型: - -``` -Peripheral(外围) ⟷ Working(工作) ⟷ Core(核心) -``` - -**晋升条件**: - -| 方向 | 条件 | -| -------------------- | ----------------------------------------------- | -| Peripheral → Working | 访问次数 ≥ 3 且 衰减分数 ≥ 0.4 | -| Working → Core | 访问次数 ≥ 10 且 衰减分数 ≥ 0.7 且 重要度 ≥ 0.8 | - -**降级条件**: - -| 方向 | 条件 | -| -------------------- | ------------------------------------------------ | -| Working → Peripheral | 衰减分数 < 0.15 或(年龄 > 60 天且访问次数 < 3) | -| Core → Working | 衰减分数 < 0.15 且 访问次数 < 3(极少触发) | - ---- - -## 四、修改文件 - -### `index.ts` — 插件入口 - -#### 新增配置项 - -```typescript -smartExtraction?: boolean; // 是否启用 LLM 智能提取(默认 true) -llm?: { - apiKey?: string; // LLM API Key(默认复用 embedding.apiKey) - model?: string; // LLM 模型(默认 gpt-4o-mini) - baseURL?: string; // LLM API 端点 -}; -extractMinMessages?: number; // 最少消息数才触发提取(默认 2) -extractMaxChars?: number; // 送入 LLM 的最大字符数(默认 8000) -``` - -#### `agent_end` 钩子改进 - -- 当 `smartExtraction` 启用时,优先使用 SmartExtractor 进行 LLM 6 类别提取 -- 当消息数不足或 SmartExtractor 未初始化时,降级回原有正则触发逻辑 -- 提取完成后输出统计日志:`smart-extracted N created, M merged, K skipped` - -#### `before_agent_start` 钩子改进 - -- 注入的记忆上下文现在显示 L0 摘要而非原始文本 -- 新增 6 类别标签(如 `[preferences:global]`) -- 新增层级标记(`[C]`ore / `[W]`orking / `[P]`eripheral) - ---- - -## 五、配置指南 - -### 最简配置(复用已有 API Key) - -```json -{ - "embedding": { - "apiKey": "${OPENAI_API_KEY}", - "model": "text-embedding-3-small" - }, - "smartExtraction": true -} -``` - -### 完整配置 - -```json -{ - "embedding": { - "apiKey": "${OPENAI_API_KEY}", - "model": "text-embedding-3-small" - }, - "smartExtraction": true, - "llm": { - "apiKey": "${OPENAI_API_KEY}", - "model": "gpt-4o-mini", - "baseURL": "https://api.openai.com/v1" - }, - "extractMinMessages": 2, - "extractMaxChars": 8000 -} -``` - -### 禁用智能提取 - -```json -{ - "smartExtraction": false -} -``` - ---- - -## 六、向后兼容性 - -| 方面 | 兼容方式 | -| -------------- | ---------------------------------------------- | -| LanceDB Schema | 新字段存储在 `metadata` JSON 中,不修改表结构 | -| 记忆类别 | 新 6 类别自动映射到原有 5 类别 | -| 混合检索 | Vector+BM25 检索管线完全保留 | -| 去重逻辑 | 仅在 `smartExtraction: true` 时生效 | -| 已有数据 | 旧记忆正常读取,新记忆额外携带 L0/L1/L2 元数据 | -| 配置 | 全部新增配置项均有默认值,零配置即可使用 | From 13e5e4a8d14ee6dc33c01eb75e85b118c2554337 Mon Sep 17 00:00:00 2001 From: TriDefender Date: Sun, 9 Aug 2026 17:14:14 +0800 Subject: [PATCH 8/8] Revert "Delete CHANGELOG-v1.1.0.md" This reverts commit 077e4f59e6ac4ed138afdf0b0c61756be2df471a. --- CHANGELOG-v1.1.0.md | 234 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 CHANGELOG-v1.1.0.md diff --git a/CHANGELOG-v1.1.0.md b/CHANGELOG-v1.1.0.md new file mode 100644 index 000000000..4b233b051 --- /dev/null +++ b/CHANGELOG-v1.1.0.md @@ -0,0 +1,234 @@ +## 1.1.0-beta.11 (OpenClaw 2026.5 runtime compatibility) + +- Ship compiled `dist/index.js` runtime and point package/OpenClaw extension entries at it. +- Declare `contracts.tools` for registered agent tools. +- Avoid double-resolving already-absolute backup/admission audit paths. +- Load LanceDB via ESM dynamic `import()` instead of `require()`. + +# memory-lancedb-pro v1.1.0 — 智能记忆增强 + +> **日期**: 2026-03-03 +> **作者**: CJY +> **概述**: 基于对 AI Agent 记忆系统的深入理解,对记忆的写入质量、生命周期管理和去重能力进行了全面改进与完善 + +--- + +## 一、改进动机 + +原有记忆系统在**检索侧**表现优异(Vector+BM25 混合检索、cross-encoder 重排序、多维评分),但在以下方面存在提升空间: + +- **记忆写入质量**:依赖正则表达式触发捕获,容易漏捕有价值信息或误捕噪声 +- **记忆结构层次**:扁平文本存储,缺乏分层索引能力 +- **记忆生命周期**:简单时间衰减,无法模拟人类记忆的遗忘与强化规律 +- **去重能力**:仅基于向量相似度的粗粒度去重,缺乏语义级判断 + +本次改进针对这三个维度进行了系统性增强。 + +--- + +## 二、变更摘要 + +| 改进维度 | 核心变更 | 效果 | +| ------------ | ----------------------------------------- | ---------------------------------- | +| 智能提取 | LLM 驱动的 6 类别提取 + L0/L1/L2 分层存储 | 记忆写入更精准、结构更丰富 | +| 生命周期管理 | Weibull 衰减模型 + 三层晋升/降级 | 重要记忆持久保留,过时记忆自然淡化 | +| 智能去重 | 向量预过滤 + LLM 语义决策 | 避免冗余记忆,支持信息演化合并 | + +--- + +## 三、新增文件 + +### 1. `src/memory-categories.ts` — 6 类别分类系统 + +设计了语义明确的记忆分类体系,将记忆分为两大类六小类: + +- **用户记忆**:`profile`(身份属性)、`preferences`(偏好习惯)、`entities`(持续存在的实体)、`events`(发生的事件) +- **Agent 记忆**:`cases`(问题-解决方案对)、`patterns`(可复用的处理流程) + +每个类别有不同的合并策略: + +- `profile` → 始终合并(用户身份信息持续累积) +- `preferences` / `entities` / `patterns` → 支持智能合并 +- `events` / `cases` → 仅新增或跳过(独立记录,保留历史完整性) + +--- + +### 2. `src/llm-client.ts` — LLM 客户端 + +封装了 LLM 调用接口,专注于结构化 JSON 输出: + +- 复用现有 OpenAI SDK 依赖,零新增包 +- 内置 JSON 容错解析:支持 markdown 代码块包裹和平衡大括号提取 +- 低温度 (0.1) 保证输出一致性 +- 30 秒超时保护,失败时优雅降级 + +--- + +### 3. `src/extraction-prompts.ts` — 记忆提取提示模板 + +精心设计了 3 个提示模板: + +| 函数 | 用途 | +| ------------------------- | --------------------------------------------------- | +| `buildExtractionPrompt()` | 从对话中提取 6 类别 L0/L1/L2 记忆,含 few-shot 示例 | +| `buildDedupPrompt()` | CREATE / MERGE / SKIP 去重决策 | +| `buildMergePrompt()` | 将新旧记忆合并为三层结构 | + +提取提示包含完整的记忆价值判断标准、类别决策逻辑表、常见混淆澄清规则和 6 个 few-shot 示例。 + +--- + +### 4. `src/smart-extractor.ts` — 智能提取管线 + +实现了完整的 LLM 驱动提取流水线: + +``` +对话文本 → LLM 提取 → 候选记忆 → 向量去重 → LLM 决策 → 持久化 +``` + +核心设计: + +- **两阶段去重**:先用向量相似度(阈值 0.7)快速筛选候选,再用 LLM 进行语义级判断 +- **类别感知合并**:不同类别应用不同合并策略 +- **L0/L1/L2 三层存储**:L0 一句话索引用于检索注入,L1 结构化摘要用于精读,L2 完整叙述用于深度回顾 +- **向后兼容**:新增的 6 类别自动映射到已有的 5 类别存储,L0/L1/L2 存储在 metadata JSON 中 +- **按类别设定重要度**:profile (0.9) > patterns (0.85) > cases/preferences (0.8) > entities (0.7) > events (0.6) + +--- + +### 5. `src/decay-engine.ts` — Weibull 衰减引擎 + +基于认知心理学中的记忆遗忘曲线研究,实现了复合衰减模型: + +**复合分数 = 时效权重 × 时效 + 频率权重 × 频率 + 内在权重 × 内在价值** + +三个分量: + +| 分量 | 机制 | 含义 | +| ------------------------ | --------------------------------- | ---------------------- | +| **时效 (recency)** | Weibull 拉伸指数衰减 `exp(-λt^β)` | 越久远的记忆衰减越快 | +| **频率 (frequency)** | 对数饱和曲线 + 时间加权 | 越常被访问的记忆越活跃 | +| **内在价值 (intrinsic)** | `importance × confidence` | 高价值记忆天然抵抗遗忘 | + +层级特定的衰减形状 (β 参数): + +- **Core** (β=0.8):亚指数衰减 → 遗忘极慢,衰减地板 0.9 +- **Working** (β=1.0):标准指数衰减,衰减地板 0.7 +- **Peripheral** (β=1.3):超指数衰减 → 遗忘加速,衰减地板 0.5 + +关键特性: + +- **重要性调制半衰期**:`effectiveHL = halfLife × exp(μ × importance)`,重要记忆持续更久 +- **搜索结果加权**:检索时自动应用衰减加权,让活跃记忆排名更高 +- **过期识别**:识别 composite < 0.3 的过期记忆 + +--- + +### 6. `src/tier-manager.ts` — 三层晋升/降级管理器 + +模拟人类记忆的多级存储模型: + +``` +Peripheral(外围) ⟷ Working(工作) ⟷ Core(核心) +``` + +**晋升条件**: + +| 方向 | 条件 | +| -------------------- | ----------------------------------------------- | +| Peripheral → Working | 访问次数 ≥ 3 且 衰减分数 ≥ 0.4 | +| Working → Core | 访问次数 ≥ 10 且 衰减分数 ≥ 0.7 且 重要度 ≥ 0.8 | + +**降级条件**: + +| 方向 | 条件 | +| -------------------- | ------------------------------------------------ | +| Working → Peripheral | 衰减分数 < 0.15 或(年龄 > 60 天且访问次数 < 3) | +| Core → Working | 衰减分数 < 0.15 且 访问次数 < 3(极少触发) | + +--- + +## 四、修改文件 + +### `index.ts` — 插件入口 + +#### 新增配置项 + +```typescript +smartExtraction?: boolean; // 是否启用 LLM 智能提取(默认 true) +llm?: { + apiKey?: string; // LLM API Key(默认复用 embedding.apiKey) + model?: string; // LLM 模型(默认 gpt-4o-mini) + baseURL?: string; // LLM API 端点 +}; +extractMinMessages?: number; // 最少消息数才触发提取(默认 2) +extractMaxChars?: number; // 送入 LLM 的最大字符数(默认 8000) +``` + +#### `agent_end` 钩子改进 + +- 当 `smartExtraction` 启用时,优先使用 SmartExtractor 进行 LLM 6 类别提取 +- 当消息数不足或 SmartExtractor 未初始化时,降级回原有正则触发逻辑 +- 提取完成后输出统计日志:`smart-extracted N created, M merged, K skipped` + +#### `before_agent_start` 钩子改进 + +- 注入的记忆上下文现在显示 L0 摘要而非原始文本 +- 新增 6 类别标签(如 `[preferences:global]`) +- 新增层级标记(`[C]`ore / `[W]`orking / `[P]`eripheral) + +--- + +## 五、配置指南 + +### 最简配置(复用已有 API Key) + +```json +{ + "embedding": { + "apiKey": "${OPENAI_API_KEY}", + "model": "text-embedding-3-small" + }, + "smartExtraction": true +} +``` + +### 完整配置 + +```json +{ + "embedding": { + "apiKey": "${OPENAI_API_KEY}", + "model": "text-embedding-3-small" + }, + "smartExtraction": true, + "llm": { + "apiKey": "${OPENAI_API_KEY}", + "model": "gpt-4o-mini", + "baseURL": "https://api.openai.com/v1" + }, + "extractMinMessages": 2, + "extractMaxChars": 8000 +} +``` + +### 禁用智能提取 + +```json +{ + "smartExtraction": false +} +``` + +--- + +## 六、向后兼容性 + +| 方面 | 兼容方式 | +| -------------- | ---------------------------------------------- | +| LanceDB Schema | 新字段存储在 `metadata` JSON 中,不修改表结构 | +| 记忆类别 | 新 6 类别自动映射到原有 5 类别 | +| 混合检索 | Vector+BM25 检索管线完全保留 | +| 去重逻辑 | 仅在 `smartExtraction: true` 时生效 | +| 已有数据 | 旧记忆正常读取,新记忆额外携带 L0/L1/L2 元数据 | +| 配置 | 全部新增配置项均有默认值,零配置即可使用 |