recallnest 2.5.4 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +34 -9
  3. package/README_CN.md +37 -12
  4. package/package.json +4 -4
  5. package/src/__tests__/access-tracker-novelty.test.ts +66 -0
  6. package/src/__tests__/api-server-security.test.ts +102 -0
  7. package/src/__tests__/belief-history.test.ts +220 -0
  8. package/src/__tests__/capture-engine.test.ts +93 -3
  9. package/src/__tests__/category-min-scores.test.ts +83 -0
  10. package/src/__tests__/cluster-consolidation.test.ts +167 -4
  11. package/src/__tests__/conflict-store.test.ts +46 -2
  12. package/src/__tests__/consolidation-engine.test.ts +97 -0
  13. package/src/__tests__/contracts.test.ts +46 -0
  14. package/src/__tests__/decay-exemption.test.ts +45 -0
  15. package/src/__tests__/dream-pipeline.test.ts +464 -5
  16. package/src/__tests__/freshness.test.ts +186 -0
  17. package/src/__tests__/hotness-blend.test.ts +207 -0
  18. package/src/__tests__/llm-client-pivot.test.ts +628 -0
  19. package/src/__tests__/maintenance-locks.test.ts +15 -0
  20. package/src/__tests__/memory-boundaries.test.ts +32 -0
  21. package/src/__tests__/noise-prototype-bank.test.ts +280 -0
  22. package/src/__tests__/pii-redact.test.ts +19 -0
  23. package/src/__tests__/pivot-apply.test.ts +373 -0
  24. package/src/__tests__/pivot-distill-supervisor.test.ts +457 -0
  25. package/src/__tests__/pivot-distill.test.ts +1178 -0
  26. package/src/__tests__/preference-matcher.test.ts +54 -2
  27. package/src/__tests__/pull-from-macbook-sync-only.test.ts +679 -0
  28. package/src/__tests__/recall-gate.test.ts +266 -0
  29. package/src/__tests__/resolve-db-path.test.ts +130 -0
  30. package/src/__tests__/resolve-tier.test.ts +64 -0
  31. package/src/__tests__/retrieval-eval.test.ts +163 -0
  32. package/src/__tests__/retriever-lazy-init-dispatch.test.ts +123 -0
  33. package/src/__tests__/skill-promotion.test.ts +53 -0
  34. package/src/__tests__/stable-context-length-contract.test.ts +62 -0
  35. package/src/__tests__/store-read-consistency.test.ts +122 -0
  36. package/src/__tests__/store-schema-migration.test.ts +87 -0
  37. package/src/__tests__/store-upsert.test.ts +17 -0
  38. package/src/__tests__/term-registry-terminal-roster.test.ts +127 -0
  39. package/src/__tests__/ui-server-security.test.ts +93 -0
  40. package/src/access-tracker.ts +53 -3
  41. package/src/adaptive-retrieval.ts +11 -0
  42. package/src/api-server.ts +9 -2
  43. package/src/auto-recall.ts +61 -1
  44. package/src/belief-history.ts +164 -0
  45. package/src/capture-engine.ts +88 -11
  46. package/src/cli.ts +109 -14
  47. package/src/conflict-engine.ts +24 -3
  48. package/src/consolidation-engine.ts +146 -12
  49. package/src/context-composer-stable-selection.ts +7 -1
  50. package/src/data-checkup.ts +1 -1
  51. package/src/decay-engine.ts +59 -7
  52. package/src/doctor.ts +4 -2
  53. package/src/dream-pipeline.ts +359 -11
  54. package/src/env-config.ts +41 -0
  55. package/src/eval.ts +154 -11
  56. package/src/freshness.ts +192 -0
  57. package/src/http-request-policy.ts +148 -0
  58. package/src/ingest.ts +277 -2
  59. package/src/kg-extractor.ts +7 -1
  60. package/src/llm-client.ts +106 -11
  61. package/src/mcp-server.ts +2 -1
  62. package/src/mcp-tools-advanced.ts +13 -0
  63. package/src/mcp-tools-core.ts +5 -1
  64. package/src/memory-boundaries.ts +23 -2
  65. package/src/memory-health-rebalance.ts +4 -0
  66. package/src/memory-output.ts +16 -2
  67. package/src/memory-store-port.ts +3 -0
  68. package/src/noise-prototype-bank.ts +408 -0
  69. package/src/pii-detector.ts +52 -2
  70. package/src/pivot-apply.ts +399 -0
  71. package/src/preference-matcher.ts +37 -3
  72. package/src/recall-gate.ts +305 -0
  73. package/src/retrieval-profiles.ts +46 -4
  74. package/src/retriever.ts +95 -18
  75. package/src/retroactive-boost.ts +6 -2
  76. package/src/runtime-config.ts +70 -5
  77. package/src/skill-promotion.ts +58 -2
  78. package/src/store.ts +54 -19
  79. package/src/term-registry.ts +35 -0
  80. package/src/ui-server.ts +7 -0
  81. package/src/version.ts +19 -0
@@ -162,7 +162,13 @@ function formatStableResult(category: StableCategory, result: RetrievalResult):
162
162
  // F1: Tag low-confidence memories in resume_context output
163
163
  const conf = getConfidence(result.entry);
164
164
  const tag = conf < 0.5 ? " [低置信]" : "";
165
- return cleanText(`${STABLE_CATEGORY_LABELS[category]}: ${text}${tag}`, 230);
165
+ // 220 是 stableContext 的 schema 上限(session-schema.ts:61),不是随手写的数:
166
+ // 这里曾是 230,比上限大 10,而 interleaveUnique(context-composer-stable.ts:199-206)
167
+ // 不再截一次 —— 于是任何 221~230 字符的行都会让 composeResumeContext 末尾的
168
+ // ResumeContextResponseSchema.parse(context-composer.ts:541)抛
169
+ // "stableContext must be at most 220 characters",整个 resume_context 调用失败。
170
+ // 改这个数之前先看 schema,两边必须一起动。
171
+ return cleanText(`${STABLE_CATEGORY_LABELS[category]}: ${text}${tag}`, 220);
166
172
  }
167
173
 
168
174
  export function selectStableResults(
@@ -98,7 +98,7 @@ function checkOrphanMemories(entries: MemoryEntry[]): CheckResult {
98
98
  function checkTierDistribution(entries: MemoryEntry[]): CheckResult {
99
99
  const tierCounts: Record<MemoryTier, number> = { core: 0, working: 0, peripheral: 0 };
100
100
  for (const e of entries) {
101
- const tier = resolveTier(e.metadata);
101
+ const tier = resolveTier(e.metadata, e.importance);
102
102
  tierCounts[tier]++;
103
103
  }
104
104
 
@@ -122,26 +122,63 @@ export function computeArousalBoost(
122
122
  // HP-7: Decay Exemptions
123
123
  // ============================================================================
124
124
 
125
+ /**
126
+ * Categories whose entries are procedural memory (knowing *how*), not declarative.
127
+ *
128
+ * Only `patterns` qualifies: RecallNest's skill schema stores skills under
129
+ * `category = "patterns"` (see memory-schema.ts), so this covers both workflow
130
+ * patterns and skills.
131
+ *
132
+ * `cases` is deliberately NOT here. A case is a problem/solution pair with a real
133
+ * episodic component ("that CI failure on 07-24"), and its solution can go stale
134
+ * when the underlying tool changes — it *should* decay. Procedural rules do not:
135
+ * see the "什么该衰减" discussion in hippo [[条件失效优于时间衰减]].
136
+ */
137
+ export const PROCEDURAL_CATEGORIES = new Set<string>(["patterns"]);
138
+
125
139
  /**
126
140
  * Check if a memory should be exempt from time decay.
127
141
  * Exempt entries keep their original score without time-based penalties.
128
142
  *
129
143
  * Exemption rules:
144
+ * 0. Procedural category → knowing-how doesn't fade with wall-clock time
130
145
  * 1. Core tier + high importance (≥ 0.95) → identity/correction, must not fade
131
146
  * 2. Recently accessed (within 7 days) → actively used, not stale
132
147
  * 3. Pinned → user explicitly marked as persistent
148
+ *
149
+ * @param category Optional entry category. Omitting it preserves the pre-Rule-0
150
+ * behaviour exactly, so existing callers stay valid.
133
151
  */
134
152
  export function isDecayExempt(
135
153
  metadata: string | undefined,
136
154
  importance: number,
155
+ category?: string,
137
156
  ): boolean {
157
+ // Rule 0: Procedural memory is exempt from *time* decay.
158
+ //
159
+ // Rules 1-3 all key off access statistics or explicit user action, which
160
+ // systematically punishes procedural memory: its defining trait is low access
161
+ // frequency + long validity ("restart launchd with bootout+sleep+bootstrap" may
162
+ // be needed once a quarter but is correct every time). Combined with `patterns`
163
+ // carrying the highest retrieval threshold in DEFAULT_CATEGORY_MIN_SCORES (0.45),
164
+ // that produced a feedback trap: ranked lower → read less → ranked lower still.
165
+ //
166
+ // Cognitive-science backing: procedural memory is non-declarative (Squire) and is
167
+ // the most decay-resistant human memory type — a different curve, not a slower one.
168
+ //
169
+ // NOTE: this exempts from *time* decay only. Procedural memory still expires, but
170
+ // by condition (its premise changed), not by age. Deliberately checked before the
171
+ // `!metadata` guard so entries without metadata are still covered.
172
+ if (category !== undefined && PROCEDURAL_CATEGORIES.has(category)) return true;
173
+
138
174
  if (!metadata) return false;
139
175
 
140
176
  try {
141
177
  const meta = JSON.parse(metadata) as Record<string, unknown>;
142
178
 
143
179
  // Rule 1: Core + very high importance
144
- const tier = meta.tier ?? resolveTierFromMeta(meta);
180
+ // importance 是本函数的入参(来自 entry 列),一并传下去——否则 heuristic 读不到它。
181
+ const tier = meta.tier ?? resolveTierFromMeta(meta, importance);
145
182
  if (tier === "core" && importance >= 0.95) return true;
146
183
 
147
184
  // Rule 2: Accessed within last 7 days
@@ -158,12 +195,21 @@ export function isDecayExempt(
158
195
  }
159
196
  }
160
197
 
161
- /** Internal helper: resolve tier from parsed metadata (avoids re-parsing). */
162
- function resolveTierFromMeta(meta: Record<string, unknown>): MemoryTier {
198
+ /**
199
+ * Internal helper: resolve tier from parsed metadata (avoids re-parsing).
200
+ *
201
+ * `entryImportance` 是 MemoryEntry 的列字段。必须由调用方传进来:importance 不存在
202
+ * 于 metadata(accessCount / lastAccessedAt / tier 才是 access-tracker 写在顶层的),
203
+ * 所以只读 meta.importance 会恒等于 0,让所有还没有显式 tier 的条目一律落到
204
+ * peripheral——2026-07 实测全库 3214 条走这个分支,顶层有 importance 的是 0 条。
205
+ */
206
+ function resolveTierFromMeta(meta: Record<string, unknown>, entryImportance?: number): MemoryTier {
163
207
  if (meta.tier === "core" || meta.tier === "working" || meta.tier === "peripheral") {
164
208
  return meta.tier;
165
209
  }
166
- const imp = typeof meta.importance === "number" ? meta.importance : 0;
210
+ const imp = typeof meta.importance === "number"
211
+ ? meta.importance
212
+ : (typeof entryImportance === "number" ? entryImportance : 0);
167
213
  const ac = typeof meta.accessCount === "number" ? meta.accessCount : 0;
168
214
  if (imp >= 0.95 || ac >= 10) return "core";
169
215
  if (imp >= 0.8 || ac >= 3) return "working";
@@ -178,13 +224,13 @@ function resolveTierFromMeta(meta: Record<string, unknown>): MemoryTier {
178
224
  * Determine a memory's current tier from its metadata.
179
225
  * Falls back to heuristic based on importance if no tier is stored.
180
226
  */
181
- export function resolveTier(metadata?: string): MemoryTier {
227
+ export function resolveTier(metadata?: string, entryImportance?: number): MemoryTier {
182
228
  if (!metadata) return "peripheral";
183
229
 
184
230
  try {
185
231
  const meta = JSON.parse(metadata) as Record<string, unknown>;
186
232
 
187
- // Explicit tier stored in metadata
233
+ // Explicit tier stored in metadata(access-tracker 算过一次就会写在这里)
188
234
  if (meta.tier === "core" || meta.tier === "working" || meta.tier === "peripheral") {
189
235
  return meta.tier;
190
236
  }
@@ -193,7 +239,13 @@ export function resolveTier(metadata?: string): MemoryTier {
193
239
  // - Pinned assets (importance ≥ 0.95) → core
194
240
  // - High importance (≥ 0.8) → working
195
241
  // - Everything else → peripheral
196
- const importance = typeof meta.importance === "number" ? meta.importance : 0;
242
+ //
243
+ // importance 优先取调用方传入的 entry 列值。metadata 里没有这个字段——顶层住着的是
244
+ // accessCount / lastAccessedAt / tier(access-tracker 写的),importance 从来不在。
245
+ // 不传就只能退回 0,那会把所有还没被访问过的条目一律压成 peripheral。
246
+ const importance = typeof meta.importance === "number"
247
+ ? meta.importance
248
+ : (typeof entryImportance === "number" ? entryImportance : 0);
197
249
  const accessCount = typeof meta.accessCount === "number" ? meta.accessCount : 0;
198
250
 
199
251
  if (importance >= 0.95 || accessCount >= 10) return "core";
package/src/doctor.ts CHANGED
@@ -14,7 +14,7 @@ import { existsSync, accessSync, constants, readFileSync } from "node:fs";
14
14
  import { resolve, join } from "node:path";
15
15
  import { homedir } from "node:os";
16
16
  import { metaDir } from "./compat.js";
17
- import { loadConfig, expandHome, resolveEnv, findConfigPath, loadDotEnv } from "./runtime-config.js";
17
+ import { loadConfig, expandHome, resolveEnv, findConfigPath, loadDotEnv, resolveDbPath } from "./runtime-config.js";
18
18
  import { createEmbedder, type EmbeddingConfig } from "./embedder.js";
19
19
  import { extractCanonicalKey, normalizeCanonicalKey } from "./memory-boundaries.js";
20
20
  import {
@@ -356,7 +356,9 @@ export async function runDoctor(options: { ci?: boolean } = {}): Promise<CheckRe
356
356
  }
357
357
 
358
358
  // 5. LanceDB data directory
359
- const dbPath = resolve(configPath ? join(configPath, "..") : process.cwd(), expandHome(config.dbPath));
359
+ // Must resolve exactly like the server does — a doctor that reports on a
360
+ // different directory than the one actually in use is worse than no doctor.
361
+ const dbPath = resolveDbPath(config);
360
362
  try {
361
363
  validateStoragePath(dbPath);
362
364
  results.push(pass("Data directory", dbPath));