recallnest 3.0.0 → 4.0.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 (246) hide show
  1. package/CHANGELOG.md +304 -0
  2. package/README.md +251 -258
  3. package/README_CN.md +226 -247
  4. package/assets/ui/app.js +6 -1
  5. package/bin/recallnest.mjs +20 -37
  6. package/docs/api-reference.md +8 -0
  7. package/docs/architecture.md +18 -13
  8. package/eval/cases-canary.json +107 -9
  9. package/integrations/examples/claude-agent-sdk/memory-agent.ts +2 -0
  10. package/integrations/examples/langchain/memory-chain.py +4 -0
  11. package/integrations/examples/openai-agents-sdk/memory-agent.py +4 -0
  12. package/package.json +12 -3
  13. package/src/activity-counter.ts +1 -0
  14. package/src/adaptive-retrieval.ts +23 -0
  15. package/src/api-server.ts +13 -11
  16. package/src/asset-sync.ts +26 -2
  17. package/src/audit-log.ts +2 -0
  18. package/src/auto-gc.ts +4 -3
  19. package/src/belief-history.ts +137 -31
  20. package/src/capture-engine.ts +459 -71
  21. package/src/cascade-forget.ts +7 -3
  22. package/src/checkpoint-mirror.ts +417 -0
  23. package/src/cli.ts +574 -54
  24. package/src/conflict-engine.ts +43 -15
  25. package/src/consolidation-engine.ts +24 -3
  26. package/src/context-collapse-renderer.ts +8 -1
  27. package/src/context-composer-stable.ts +5 -1
  28. package/src/context-composer.ts +11 -2
  29. package/src/distill-lock.ts +246 -74
  30. package/src/dream-pipeline.ts +3 -1
  31. package/src/embedder.ts +24 -0
  32. package/src/env-config.ts +136 -0
  33. package/src/flock.ts +97 -0
  34. package/src/forget-engine.ts +342 -58
  35. package/src/gateway-server.ts +4 -0
  36. package/src/ingest-plan.ts +56 -0
  37. package/src/ingest.ts +294 -121
  38. package/src/kg-contradiction.ts +753 -0
  39. package/src/kg-extractor.ts +17 -0
  40. package/src/kg-store.ts +1 -1
  41. package/src/known-item-eval.ts +455 -0
  42. package/src/llm-client.ts +13 -26
  43. package/src/llm-consolidation.ts +2 -2
  44. package/src/mcp-tool-deps.ts +6 -0
  45. package/src/mcp-tools-advanced.ts +80 -45
  46. package/src/mcp-tools-core.ts +23 -17
  47. package/src/mcp-tools-governance.ts +13 -7
  48. package/src/memory-assets.ts +144 -8
  49. package/src/memory-boundaries.ts +19 -0
  50. package/src/memory-evolution.ts +9 -0
  51. package/src/memory-lint.ts +1 -1
  52. package/src/memory-output.ts +311 -14
  53. package/src/memory-reconcile.ts +1437 -0
  54. package/src/memory-schema.ts +11 -0
  55. package/src/memory-store-port.ts +1 -0
  56. package/src/minis-archive.ts +151 -0
  57. package/src/noise-filter.ts +15 -2
  58. package/src/pii-detector.ts +31 -1
  59. package/src/pivot-apply.ts +55 -5
  60. package/src/pivot-evidence.ts +128 -0
  61. package/src/ppr-traversal.ts +25 -1
  62. package/src/preference-matcher.ts +40 -28
  63. package/src/prospective-memory.ts +7 -4
  64. package/src/retriever.ts +297 -57
  65. package/src/retroactive-boost.ts +3 -2
  66. package/src/runtime-config.ts +15 -1
  67. package/src/scope-policy.ts +1 -0
  68. package/src/session-distiller.ts +6 -2
  69. package/src/session-engine.ts +11 -4
  70. package/src/session-output.ts +20 -1
  71. package/src/session-schema.ts +5 -2
  72. package/src/session-store.ts +59 -12
  73. package/src/skill-promotion.ts +67 -1
  74. package/src/store-receipts.ts +109 -0
  75. package/src/store-write-budget.ts +37 -0
  76. package/src/store.ts +298 -57
  77. package/src/text-fingerprint.ts +25 -0
  78. package/src/trigger-store.ts +443 -0
  79. package/src/ui-server.ts +22 -21
  80. package/src/version-manager.ts +19 -5
  81. package/src/write-verifier.ts +16 -11
  82. package/src/__tests__/access-tracker-novelty.test.ts +0 -324
  83. package/src/__tests__/activity-counter.test.ts +0 -125
  84. package/src/__tests__/adaptive-fidelity.test.ts +0 -102
  85. package/src/__tests__/admission-control.test.ts +0 -206
  86. package/src/__tests__/age-label.test.ts +0 -33
  87. package/src/__tests__/anchor-generator.test.ts +0 -67
  88. package/src/__tests__/api-server-security.test.ts +0 -102
  89. package/src/__tests__/auto-consolidation.test.ts +0 -168
  90. package/src/__tests__/auto-recall.test.ts +0 -131
  91. package/src/__tests__/babel-integration.test.ts +0 -144
  92. package/src/__tests__/batch-dedup.test.ts +0 -65
  93. package/src/__tests__/belief-history.test.ts +0 -220
  94. package/src/__tests__/canary-eval.test.ts +0 -114
  95. package/src/__tests__/capture-engine.test.ts +0 -1328
  96. package/src/__tests__/capture-heuristic.test.ts +0 -171
  97. package/src/__tests__/cascade-forget.test.ts +0 -210
  98. package/src/__tests__/category-min-scores.test.ts +0 -116
  99. package/src/__tests__/cc8-cc1-essential-context.test.ts +0 -298
  100. package/src/__tests__/circuit-breaker.test.ts +0 -99
  101. package/src/__tests__/cluster-consolidation.test.ts +0 -1236
  102. package/src/__tests__/confidence-metadata.test.ts +0 -125
  103. package/src/__tests__/confidence-tracker.test.ts +0 -261
  104. package/src/__tests__/conflict-advisor.test.ts +0 -206
  105. package/src/__tests__/conflict-escalation.test.ts +0 -149
  106. package/src/__tests__/conflict-lifecycle.test.ts +0 -83
  107. package/src/__tests__/conflict-output.test.ts +0 -85
  108. package/src/__tests__/conflict-store.test.ts +0 -418
  109. package/src/__tests__/connector-v1.test.ts +0 -181
  110. package/src/__tests__/consolidation-engine.test.ts +0 -520
  111. package/src/__tests__/constructive-retrieval.test.ts +0 -509
  112. package/src/__tests__/context-collapse-renderer.test.ts +0 -214
  113. package/src/__tests__/context-composer-pins.test.ts +0 -64
  114. package/src/__tests__/context-composer-task-selection.test.ts +0 -24
  115. package/src/__tests__/context-reconstructor.test.ts +0 -65
  116. package/src/__tests__/context-renderer.test.ts +0 -91
  117. package/src/__tests__/continuity-eval.test.ts +0 -275
  118. package/src/__tests__/continuity-schema.test.ts +0 -97
  119. package/src/__tests__/contracts.test.ts +0 -262
  120. package/src/__tests__/conversation-importer.test.ts +0 -692
  121. package/src/__tests__/core-recall-eval.test.ts +0 -235
  122. package/src/__tests__/core-summary.test.ts +0 -109
  123. package/src/__tests__/data-checkup.test.ts +0 -304
  124. package/src/__tests__/decay-exemption.test.ts +0 -129
  125. package/src/__tests__/dedup-actions.test.ts +0 -58
  126. package/src/__tests__/dedup-guard.test.ts +0 -39
  127. package/src/__tests__/deterministic-id.test.ts +0 -39
  128. package/src/__tests__/distill-lock.test.ts +0 -305
  129. package/src/__tests__/distill-session-argshape.test.ts +0 -79
  130. package/src/__tests__/doctor.test.ts +0 -319
  131. package/src/__tests__/dream-pipeline.test.ts +0 -820
  132. package/src/__tests__/dream-scope-isolation.test.ts +0 -386
  133. package/src/__tests__/dream-write-batching.test.ts +0 -211
  134. package/src/__tests__/drill-down.test.ts +0 -108
  135. package/src/__tests__/embedder.test.ts +0 -122
  136. package/src/__tests__/emotion-decay.test.ts +0 -46
  137. package/src/__tests__/emotion-detector.test.ts +0 -56
  138. package/src/__tests__/emotion-phase1-integration.test.ts +0 -153
  139. package/src/__tests__/emotion-retrieval.test.ts +0 -56
  140. package/src/__tests__/entity-resolver.test.ts +0 -58
  141. package/src/__tests__/env-config-invariant.test.ts +0 -80
  142. package/src/__tests__/env-config.test.ts +0 -211
  143. package/src/__tests__/error-signature-boost.test.ts +0 -87
  144. package/src/__tests__/error-signature.test.ts +0 -95
  145. package/src/__tests__/error-taxonomy.test.ts +0 -129
  146. package/src/__tests__/ethics-layer.test.ts +0 -476
  147. package/src/__tests__/event-segmenter.test.ts +0 -153
  148. package/src/__tests__/feature-flags-integration.test.ts +0 -54
  149. package/src/__tests__/frequency-tracker.test.ts +0 -113
  150. package/src/__tests__/freshness.test.ts +0 -186
  151. package/src/__tests__/graph-export.test.ts +0 -489
  152. package/src/__tests__/hotness-blend.test.ts +0 -283
  153. package/src/__tests__/ingest-chunking.test.ts +0 -44
  154. package/src/__tests__/ingest-scope-prefix.test.ts +0 -69
  155. package/src/__tests__/ingest.test.ts +0 -360
  156. package/src/__tests__/interference-detector.test.ts +0 -203
  157. package/src/__tests__/kg-extractor.test.ts +0 -192
  158. package/src/__tests__/kg-store.test.ts +0 -300
  159. package/src/__tests__/llm-client-pivot.test.ts +0 -628
  160. package/src/__tests__/llm-consolidation.test.ts +0 -226
  161. package/src/__tests__/maintenance-locks.test.ts +0 -197
  162. package/src/__tests__/mcp-efficiency.test.ts +0 -288
  163. package/src/__tests__/mcp-related-scopes.test.ts +0 -151
  164. package/src/__tests__/memory-boundaries.test.ts +0 -282
  165. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -362
  166. package/src/__tests__/memory-evolution.test.ts +0 -271
  167. package/src/__tests__/memory-health-rebalance.test.ts +0 -144
  168. package/src/__tests__/memory-lint.test.ts +0 -421
  169. package/src/__tests__/memory-output.test.ts +0 -224
  170. package/src/__tests__/memory-promotion.test.ts +0 -315
  171. package/src/__tests__/memory-utility.test.ts +0 -256
  172. package/src/__tests__/migrate-dedup-ids.test.ts +0 -65
  173. package/src/__tests__/multi-vector.test.ts +0 -242
  174. package/src/__tests__/narrative-architecture.test.ts +0 -456
  175. package/src/__tests__/noise-filter.test.ts +0 -205
  176. package/src/__tests__/noise-prototype-bank.test.ts +0 -280
  177. package/src/__tests__/obsidian-connector.test.ts +0 -363
  178. package/src/__tests__/omitted-reporting.test.ts +0 -98
  179. package/src/__tests__/openai-contract.test.ts +0 -513
  180. package/src/__tests__/pending-review.test.ts +0 -77
  181. package/src/__tests__/pii-redact.test.ts +0 -333
  182. package/src/__tests__/pivot-apply.test.ts +0 -373
  183. package/src/__tests__/pivot-distill-supervisor.test.ts +0 -457
  184. package/src/__tests__/pivot-distill.test.ts +0 -1178
  185. package/src/__tests__/post-retrieval-filter.test.ts +0 -178
  186. package/src/__tests__/ppr-traversal.test.ts +0 -256
  187. package/src/__tests__/prediction-engine.test.ts +0 -349
  188. package/src/__tests__/predictive-prospective-memory.test.ts +0 -416
  189. package/src/__tests__/preference-guard-smoke.test.ts +0 -211
  190. package/src/__tests__/preference-matcher.test.ts +0 -282
  191. package/src/__tests__/preference-slots.test.ts +0 -205
  192. package/src/__tests__/promotion-distinct-sources.test.ts +0 -162
  193. package/src/__tests__/prospective-memory.test.ts +0 -283
  194. package/src/__tests__/pull-from-macbook-sync-only.test.ts +0 -679
  195. package/src/__tests__/query-entity-detector.test.ts +0 -171
  196. package/src/__tests__/query-expander-alias.test.ts +0 -146
  197. package/src/__tests__/query-expander.test.ts +0 -34
  198. package/src/__tests__/recall-gate.test.ts +0 -266
  199. package/src/__tests__/recall-governor.test.ts +0 -227
  200. package/src/__tests__/recall-mode.test.ts +0 -64
  201. package/src/__tests__/resolve-db-path.test.ts +0 -130
  202. package/src/__tests__/resolve-tier.test.ts +0 -64
  203. package/src/__tests__/result-synthesizer.test.ts +0 -198
  204. package/src/__tests__/resume-context.test.ts +0 -4898
  205. package/src/__tests__/retrieval-eval.test.ts +0 -163
  206. package/src/__tests__/retrieval-trace.test.ts +0 -213
  207. package/src/__tests__/retrieve-audit-revision.test.ts +0 -255
  208. package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
  209. package/src/__tests__/retriever-lazy-init-dispatch.test.ts +0 -123
  210. package/src/__tests__/retroactive-boost.test.ts +0 -223
  211. package/src/__tests__/salience-scoring.test.ts +0 -106
  212. package/src/__tests__/scope-inventory.test.ts +0 -200
  213. package/src/__tests__/scope-policy.test.ts +0 -69
  214. package/src/__tests__/scope-suggester.test.ts +0 -80
  215. package/src/__tests__/session-checkpoint.test.ts +0 -291
  216. package/src/__tests__/session-distiller.test.ts +0 -640
  217. package/src/__tests__/session-gc.test.ts +0 -215
  218. package/src/__tests__/skill-engine.test.ts +0 -552
  219. package/src/__tests__/skill-promotion.test.ts +0 -654
  220. package/src/__tests__/skill-verifier.test.ts +0 -122
  221. package/src/__tests__/source-heartbeat.test.ts +0 -254
  222. package/src/__tests__/stable-context-length-contract.test.ts +0 -62
  223. package/src/__tests__/store-patch-metadata.test.ts +0 -235
  224. package/src/__tests__/store-read-consistency.test.ts +0 -122
  225. package/src/__tests__/store-schema-migration.test.ts +0 -87
  226. package/src/__tests__/store-scope-audit.test.ts +0 -81
  227. package/src/__tests__/store-upsert.test.ts +0 -201
  228. package/src/__tests__/supersede-chain.test.ts +0 -154
  229. package/src/__tests__/synthesis-contract.test.ts +0 -252
  230. package/src/__tests__/synthesis-promotion.test.ts +0 -369
  231. package/src/__tests__/temporal-parser.test.ts +0 -241
  232. package/src/__tests__/temporal-validity.test.ts +0 -160
  233. package/src/__tests__/term-registry-terminal-roster.test.ts +0 -127
  234. package/src/__tests__/term-registry.test.ts +0 -121
  235. package/src/__tests__/tool-error-surfacing.test.ts +0 -98
  236. package/src/__tests__/tool-output-compressor.test.ts +0 -160
  237. package/src/__tests__/topic-tag.test.ts +0 -113
  238. package/src/__tests__/trusted-memory.test.ts +0 -867
  239. package/src/__tests__/ui-server-security.test.ts +0 -93
  240. package/src/__tests__/usage-tracker.test.ts +0 -287
  241. package/src/__tests__/utility-weight.test.ts +0 -57
  242. package/src/__tests__/verbatim-self-recall.test.ts +0 -279
  243. package/src/__tests__/version-manager.test.ts +0 -248
  244. package/src/__tests__/workflow-observation-skill-outcome.test.ts +0 -352
  245. package/src/__tests__/workflow-observation.test.ts +0 -433
  246. package/src/__tests__/write-verifier.test.ts +0 -122
@@ -225,6 +225,14 @@ Store a new durable memory entry.
225
225
 
226
226
  If the same `canonicalKey` is already occupied by a different durable category, RecallNest returns `disposition = "conflict"` plus a `conflictId` instead of silently creating a second durable owner.
227
227
 
228
+ `stored` is `true` (status `201`) only when the write landed new content: `disposition` is `stored`, `updated` or `promoted`. Otherwise the status is `200` and `stored` is `false`:
229
+
230
+ | `disposition` | Meaning | `id` |
231
+ |---|---|---|
232
+ | `deduped` | The content already exists; no new row was written | the existing memory |
233
+ | `conflict` | Canonical key owned by another category; parked in the conflict store | the existing owner |
234
+ | `rejected` | Turned away by admission control; `rejectionReason` says why (`text_too_short`, `noise_detected`, `importance_too_low`, `rate_limited`) | a placeholder, no row exists |
235
+
228
236
  For slot-aware preferences, RecallNest now derives structured keys instead of collapsing everything into a broader same-topic key. Examples:
229
237
 
230
238
  - atomic brand-item preference: `我喜欢吃麦当劳的麦辣鸡翅` -> `preferences:brand-item:麦当劳:麦辣鸡翅`
@@ -47,9 +47,12 @@ See [memory-boundary-contract.md](./memory-boundary-contract.md).
47
47
  │ │
48
48
  │ ┌─────────────┐ ┌──────────────┐ ┌────────────────────┐ │
49
49
  │ │ Retriever │ │ Classifier │ │ Query Expander │ │
50
- │ │ (hybrid: │ │ (6 categories│ │ (synonym + │ │
51
- │ │ vector + │ │ auto-assign)│ │ semantic expand) │ │
52
- │ │ BM25 + RRF) │ │ │ │ │ │
50
+ │ │ (default: │ │ (6 categories│ │ (synonym + │ │
51
+ │ │ vector-only;│ │ auto-assign)│ │ semantic expand) │ │
52
+ │ │ optional │ │ │ │ │ │
53
+ │ │ hybrid: │ │ │ │ │ │
54
+ │ │ weighted │ │ │ │ │ │
55
+ │ │ vector+BM25)│ │ │ │ │ │
53
56
  │ └──────┬───────┘ └──────────────┘ └────────────────────┘ │
54
57
  │ │ │
55
58
  │ ┌──────┴───────┐ ┌──────────────┐ ┌────────────────────┐ │
@@ -116,13 +119,15 @@ Query Results
116
119
  │
117
120
  ▼
118
121
  ┌──────────────┐ ┌──────────────┐ ┌───────────────┐
119
- │ Query │───►│ Hybrid │───►│ Post-process │──► Top-K
120
- │ Expander │ │ Retrieval │ │ │ Results
121
- │ (synonyms) │ │ │ │ - Decay │
122
- └──────────────┘ │ Vector: 0.7 │ │ - Access boost│
123
- │ BM25: 0.3 │ │ - Score floor │
124
- │ RRF merge │ │ - Dedup │
125
- └──────────────┘ └───────────────┘
122
+ │ Query │───►│ Retrieval │───►│ Post-process │──► Top-K
123
+ │ Expander │ │ default: │ │ │ Results
124
+ │ (synonyms) │ │ vector-only │ │ - Decay │
125
+ └──────────────┘ │ hybrid mode: │ │ - Access boost│
126
+ │ Vector: 0.7 │ │ - Score floor │
127
+ │ BM25: 0.3 │ │ - Dedup │
128
+ │ weighted │ └───────────────┘
129
+ │ fusion │
130
+ └──────────────┘
126
131
  ```
127
132
 
128
133
  ## Key Design Decisions
@@ -131,8 +136,8 @@ Query Results
131
136
  |----------|-----------|
132
137
  | **LanceDB** (not SQLite/Postgres) | Native vector search, columnar storage, zero-config, single-file DB |
133
138
  | **Jina v5** (not OpenAI embeddings) | Task-aware embeddings (query vs passage), better multilingual, 1024-dim sweet spot |
134
- | **Hybrid retrieval** (vector + BM25) | Vector alone misses keyword matches; BM25 alone misses semantic similarity |
135
- | **RRF fusion** | Reciprocal Rank Fusion is parameter-free and robust across score distributions |
139
+ | **Hybrid retrieval** (vector + BM25, optional) | Vector alone misses keyword matches; BM25 alone misses semantic similarity. Default `retrieval.mode` is `"vector"` (vector-only search; `DEFAULT_RETRIEVAL_CONFIG` in `src/retriever.ts`, `config.json.example`). Set `retrieval.mode` to `"hybrid"` in `config.json` to add BM25 full-text search; if the store has no full-text index, retrieval stays vector-only |
140
+ | **Weighted score fusion** (hybrid mode only) | Applies only when `retrieval.mode` is `"hybrid"` (default is `"vector"`). Hybrid mode combines vector and BM25 scores as a weighted average (defaults `vectorWeight: 0.7`, `bm25Weight: 0.3`), with a 5% bonus when both legs return the same memory; a memory found by only one leg is scored by that leg alone (BM25-only hits get ×1.15 on short queries). For short queries (≤ 4 tokens) the vector weight is multiplied by 0.7 and the BM25 weight by 1.5, capped at 0.6 (`fuseResults()` in `src/retriever.ts`) |
136
141
  | **Weibull decay** (not exponential) | Better models human forgetting: slow start, accelerating fade |
137
142
  | **6 categories** (not free-form tags) | Structured enough for filtering and lifecycle rules, simple enough to auto-classify |
138
143
  | **HTTP API + MCP** (not just MCP) | MCP is great for CLI tools, but HTTP API works with any language/framework |
@@ -146,7 +151,7 @@ src/
146
151
  ├── mcp-server.ts # MCP server (stdio transport)
147
152
  ├── cli.ts # CLI entry point (lm command)
148
153
  ├── store.ts # LanceDB storage layer
149
- ├── retriever.ts # Hybrid retrieval (vector + BM25 + RRF)
154
+ ├── retriever.ts # Hybrid retrieval (vector + BM25, weighted score fusion)
150
155
  ├── embedder.ts # Jina embedding client
151
156
  ├── ingest.ts # Multi-source ingestion pipeline
152
157
  ├── chunker.ts # Text chunking with noise filtering
@@ -4,7 +4,9 @@
4
4
  "query": "mem0 对 recallnest 有没有借鉴作用",
5
5
  "profile": "fact-check",
6
6
  "limit": 8,
7
- "targets": ["a2fe9b62"],
7
+ "targets": [
8
+ "a2fe9b62"
9
+ ],
8
10
  "expectTopK": 3,
9
11
  "notes": "A 单目标:mem0→recallnest 借鉴审计结论该被召回。"
10
12
  },
@@ -13,7 +15,9 @@
13
15
  "query": "recallnest 本地两个目录反复踩坑是怎么解决的",
14
16
  "profile": "debug",
15
17
  "limit": 8,
16
- "targets": ["dbcb317a"],
18
+ "targets": [
19
+ "dbcb317a"
20
+ ],
17
21
  "expectTopK": 3,
18
22
  "notes": "A 单目标:dual-dir 符号链接统一 case(dbcb317a, cases, imp 0.9, baseline rank1)。原 target e40ab64b 系标注错——e40ab64b 实为『RecallNest 演进优先 workflow pattern』,与本 query 无关;dbcb317a 才是『~/recallnest→~/Projects/recallnest symlink 统一』。"
19
23
  },
@@ -22,8 +26,13 @@
22
26
  "query": "recallnest 的 capture 是纯 heuristic 吗,提取到底用不用 LLM",
23
27
  "profile": "fact-check",
24
28
  "limit": 8,
25
- "targets": ["727da608"],
26
- "expectOrder": ["727da608", "a2fe9b62"],
29
+ "targets": [
30
+ "727da608"
31
+ ],
32
+ "expectOrder": [
33
+ "727da608",
34
+ "a2fe9b62"
35
+ ],
27
36
  "expectTopK": 3,
28
37
  "notes": "B 新旧对:727da608(cases,imp0.8) 正文明确『修正 a2fe9b62 中 capture 纯heuristic/zero-LLM 的不准确表述』,是本 query 准确答案;a2fe9b62(events,imp0.72,mem0借鉴审计) 含被纠正的旧表述。expectOrder 测『两者同时召回时准确版 727 须排在旧版 a2 前』——纯排名对比(rank 唯一,<与≤等价),不靠结构化 supersede。校准修正:原 notes『验 confidence/recency/decay』机制声称不成立(二者无 evolution 链/降权链,靠内容质量排序);且主话题不重合(727=ingest去重史 / a2=mem0审计),实测 a2 常落选(baseline rank=null),故本 case 当前主要测 727 召回质量+弱新旧序保护,非真正新旧竞争。理想 B 类应换一对同话题、会同时召回的新旧记忆(P1 后续)。"
29
38
  },
@@ -32,8 +41,12 @@
32
41
  "query": "工作记忆要不要做 scope 隔离",
33
42
  "profile": "default",
34
43
  "limit": 8,
35
- "targets": ["491c8b30"],
36
- "hardNegatives": ["b59e86b2"],
44
+ "targets": [
45
+ "491c8b30"
46
+ ],
47
+ "hardNegatives": [
48
+ "b59e86b2"
49
+ ],
37
50
  "expectTopK": 2,
38
51
  "notes": "C 干扰项:491c8b30(元决策『工作记忆不做 scope 隔离』,baseline rank1) 应召回。hardNegative b59e86b2(entities,imp0.65,scope=codex:019cfba5) 是 codex 端关于 scope 模型/associative access/隔离的设计讨论片段——词面同话题、别处语境(非 Alice 元决策),baseline rank2 进了 top。补此 hardNeg 后本 case 在当前裸 retrieve 下会 fail,暴露『跨-scope 旁支讨论被召回进 top』的真问题(契合 P0 scope 主题)。"
39
52
  },
@@ -42,7 +55,13 @@
42
55
  "query": "Alice 的写作风格偏好是什么",
43
56
  "profile": "writing",
44
57
  "limit": 6,
45
- "expectContentAny": ["口语化", "不端着", "剧评腔", "书面非口语", "可自嘲"],
58
+ "expectContentAny": [
59
+ "口语化",
60
+ "不端着",
61
+ "剧评腔",
62
+ "书面非口语",
63
+ "可自嘲"
64
+ ],
46
65
  "expectTopK": 3,
47
66
  "notes": "D 稳定偏好:不钉 id,召回文本命中要点即算对。"
48
67
  },
@@ -51,8 +70,87 @@
51
70
  "query": "Alice 的配图审美偏好",
52
71
  "profile": "default",
53
72
  "limit": 6,
54
- "expectContentAny": ["手绘", "撞色", "高对比", "静物无人物", "涂鸦"],
73
+ "expectContentAny": [
74
+ "手绘",
75
+ "撞色",
76
+ "高对比",
77
+ "静物无人物",
78
+ "涂鸦"
79
+ ],
55
80
  "expectTopK": 3,
56
81
  "notes": "D 稳定偏好:同上。"
82
+ },
83
+ {
84
+ "name": "canary-assoc-onboarding-written",
85
+ "query": "接新的 AI 是不是又要从头讲一遍",
86
+ "profile": "default",
87
+ "scope": "memory:pivot",
88
+ "limit": 8,
89
+ "targets": [
90
+ "5a9affa2"
91
+ ],
92
+ "expectTopK": 5,
93
+ "notes": "assoc 对照组:query 就是正文尾巴写下的『她以后会这么问起』原句。09-22 基线 #1。"
94
+ },
95
+ {
96
+ "name": "canary-assoc-onboarding-zero",
97
+ "query": "新接的那个 app 答应会遵守约定,结果每次开新对话都当没听过",
98
+ "profile": "default",
99
+ "scope": "memory:pivot",
100
+ "limit": 8,
101
+ "targets": [
102
+ "5a9affa2"
103
+ ],
104
+ "expectTopK": 5,
105
+ "notes": "assoc 零词面重合:三层规则结构 / 口头承诺不是机制。09-22 基线 #1(path=vector)。"
106
+ },
107
+ {
108
+ "name": "canary-assoc-rebuild-pool",
109
+ "query": "要不要把老的那十几万条清一清",
110
+ "profile": "default",
111
+ "scope": "memory:pivot",
112
+ "limit": 8,
113
+ "targets": [
114
+ "9f908ad2"
115
+ ],
116
+ "expectTopK": 5,
117
+ "notes": "assoc:08-01 从零建池不修补存量、12.9 万条不删。09-22 基线 #1。"
118
+ },
119
+ {
120
+ "name": "canary-assoc-commit-gate",
121
+ "query": "记忆写进去之前要不要先让人审一下再入库",
122
+ "profile": "default",
123
+ "scope": "memory:pivot",
124
+ "limit": 8,
125
+ "targets": [
126
+ "545562b9",
127
+ "a81ccffb"
128
+ ],
129
+ "expectTopK": 5,
130
+ "notes": "assoc:提案/提交分离早就有 + deepbrain 否决。09-22 基线 #1 / #5。"
131
+ },
132
+ {
133
+ "name": "canary-assoc-benchmark-tmem",
134
+ "query": "T-Mem 论文里那个榜单我们能不能也上去比比",
135
+ "profile": "default",
136
+ "scope": "memory:pivot",
137
+ "limit": 8,
138
+ "targets": [
139
+ "64df5691"
140
+ ],
141
+ "expectTopK": 5,
142
+ "notes": "assoc 已知坏样本①:07-25 公开 benchmark 否决,正文尾巴已带『她以后可能会这样问起』句。09-22 基线 #7(limit 5 漏)。"
143
+ },
144
+ {
145
+ "name": "canary-assoc-benchmark-tencent",
146
+ "query": "腾讯那篇论文的榜我们要不要也去排个名",
147
+ "profile": "default",
148
+ "scope": "memory:pivot",
149
+ "limit": 8,
150
+ "targets": [
151
+ "64df5691"
152
+ ],
153
+ "expectTopK": 5,
154
+ "notes": "assoc 已知坏样本②:同上目标,措辞换成『榜 / 排名』。09-22 基线 top-10 不出现。"
57
155
  }
58
- ]
156
+ ]
@@ -113,6 +113,8 @@ async function handleTool(
113
113
  }),
114
114
  });
115
115
  if (!res.ok) return `Error: ${res.status} ${await res.text()}`;
116
+ const data = await res.json() as { stored: boolean; disposition: string; id: string; rejectionReason?: string };
117
+ if (!data.stored) return `Memory not stored (${data.disposition}: ${data.rejectionReason ?? data.id}).`;
116
118
  return "Memory stored successfully.";
117
119
  }
118
120
 
@@ -78,6 +78,10 @@ def store_memory(text: str, category: str = "events") -> str:
78
78
  },
79
79
  )
80
80
  r.raise_for_status()
81
+ data = r.json()
82
+ if not data.get("stored"):
83
+ reason = data.get("rejectionReason") or data.get("id")
84
+ return f"Memory not stored ({data.get('disposition')}: {reason})."
81
85
  return "Memory stored successfully."
82
86
 
83
87
 
@@ -79,6 +79,10 @@ async def store_memory(text: str, category: str = "events") -> str:
79
79
  },
80
80
  )
81
81
  r.raise_for_status()
82
+ data = r.json()
83
+ if not data.get("stored"):
84
+ reason = data.get("rejectionReason") or data.get("id")
85
+ return f"Memory not stored ({data.get('disposition')}: {reason})."
82
86
  return "Memory stored successfully."
83
87
 
84
88
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "recallnest",
3
- "version": "3.0.0",
3
+ "version": "4.0.0",
4
4
  "description": "RecallNest — MCP-native, local-first memory search and distillation for AI conversations",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -22,7 +22,8 @@
22
22
  "eval/continuity/*.json",
23
23
  "integrations/",
24
24
  "README_CN.md",
25
- "src/"
25
+ "src/",
26
+ "!src/**/__tests__/**"
26
27
  ],
27
28
  "scripts": {
28
29
  "search": "bun run src/cli.ts search",
@@ -45,6 +46,7 @@
45
46
  "eval": "bun run src/eval.ts",
46
47
  "eval:continuity": "bun run src/eval.ts --mode continuity --cases eval/continuity/cases.json",
47
48
  "eval:canary": "bun run src/eval.ts --mode canary --cases eval/cases-canary.json",
49
+ "eval:known-item": "bun run eval/known-item.ts run",
48
50
  "profiles": "bun run src/cli.ts profiles",
49
51
  "doctor": "bun run src/cli.ts doctor",
50
52
  "demo": "bun run src/cli.ts demo",
@@ -60,10 +62,17 @@
60
62
  "engines": {
61
63
  "node": ">=22"
62
64
  },
65
+ "overrides": {
66
+ "fast-uri": "^3.1.8",
67
+ "qs": "^6.16.0",
68
+ "hono": "^4.13.7",
69
+ "ip-address": "^10.7.1",
70
+ "proxy-addr": "^2.0.8"
71
+ },
63
72
  "dependencies": {
64
73
  "@hono/node-server": "1.19.17",
65
74
  "@lancedb/lancedb": "^0.26.0",
66
- "@modelcontextprotocol/sdk": "^1.30.0",
75
+ "@modelcontextprotocol/sdk": "^1.31.0",
67
76
  "commander": "^12.0.0",
68
77
  "openai": "^7.5.0",
69
78
  "tsx": "^4.19.0",
@@ -104,6 +104,7 @@ const ACTIVITY_LOCK_KEY = "activity-stats";
104
104
  const ACTIVITY_LOCK_OPTS = { onBusy: "wait" as const, waitTimeoutMs: 2_000, expireMs: 30_000 };
105
105
 
106
106
  async function withStatsLock<T>(fn: () => T): Promise<T> {
107
+ // lock-wait: no store-write inside — fn only reads and rewrites the activity stats file.
107
108
  const outcome = await withLock(ACTIVITY_LOCK_KEY, async () => fn(), ACTIVITY_LOCK_OPTS);
108
109
  // onBusy:"wait" resolves ran:true or throws on timeout; ran:false unreachable.
109
110
  if (!outcome.ran) throw new Error("activity-stats lock unavailable");
@@ -74,3 +74,26 @@ export function shouldSkipRetrieval(query: string, minLength?: number): boolean
74
74
 
75
75
  return false;
76
76
  }
77
+
78
+ /**
79
+ * 检索入口用的判定:这条检索要不要直接返回空。
80
+ *
81
+ * 上面那套规则是「一条聊天消息值不值得自动检索」的判定(省嵌入调用),只该管自动召回。
82
+ * 人主动发起的查询(MCP search_memory、CLI、HTTP、本地 UI)以 `git` / `docker` / `test` 开头、
83
+ * 纯数字或 `#22`、不含中文又短于 15 字的短语,都是正常的查法;被闸掉时返回的空结果和
84
+ * 「库里确实没有」看不出区别。所以:
85
+ * - `trivialQueryGate` 显式给了就按它;
86
+ * - 没给时只在 `source === "auto-recall"` 时开闸——`source` 同时管着三处强化写入,
87
+ * 有些人主动发起的检索(search_memory 的相关 scope 旁路、known-item eval)为了不写库也标成
88
+ * auto-recall,它们用 `trivialQueryGate: false` 把闸关掉;
89
+ * - 空串与纯空白不管哪条路都跳过。
90
+ */
91
+ export function shouldSkipRetrievalFor(context: {
92
+ query: string;
93
+ source?: "manual" | "auto-recall" | "cli";
94
+ trivialQueryGate?: boolean;
95
+ }): boolean {
96
+ if (context.query.trim().length === 0) return true;
97
+ const gate = context.trivialQueryGate ?? context.source === "auto-recall";
98
+ return gate && shouldSkipRetrieval(context.query);
99
+ }
package/src/api-server.ts CHANGED
@@ -7,10 +7,13 @@
7
7
  */
8
8
 
9
9
  import { createComponentResolver, loadConfig, loadDotEnv } from "./runtime-config.js";
10
+ import { HTTP_WRITE_IDLE_TIMEOUT_S } from "./store-write-budget.js";
10
11
  import { DurableMemoryCategorySchema } from "./memory-schema.js";
11
12
  import { persistCaseMemory, persistMemory, persistMemoryBatch, persistWorkflowPattern, promoteMemory } from "./capture-engine.js";
12
13
  import { buildSessionCheckpointResult } from "./session-engine.js";
14
+ import { buildStoreHttpResponse } from "./store-receipts.js";
13
15
  import { SessionCheckpointStore } from "./session-store.js";
16
+ import { mirrorCheckpointWithDeadline } from "./checkpoint-mirror.js";
14
17
  import { composeResumeContext } from "./context-composer.js";
15
18
  import { extractMemoryProvenance } from "./memory-boundaries.js";
16
19
  import { ConflictStatusSchema } from "./conflict-schema.js";
@@ -242,17 +245,8 @@ async function handleStore(request: Request): Promise<Response> {
242
245
  try {
243
246
  const { store, embedder } = getComponents();
244
247
  const stored = await persistMemory({ store, embedder, conflictStore, auditLogger }, body);
245
-
246
- return jsonResponse({
247
- id: stored.id,
248
- stored: stored.disposition !== "conflict",
249
- disposition: stored.disposition,
250
- storedAt: stored.storedAt,
251
- category: stored.category,
252
- scope: stored.resolvedScope,
253
- canonicalKey: stored.canonicalKey,
254
- conflictId: stored.conflictId,
255
- }, 201);
248
+ const response = buildStoreHttpResponse(stored);
249
+ return jsonResponse(response.body, response.status);
256
250
  } catch (err) {
257
251
  const message = err instanceof Error ? err.message : String(err);
258
252
  return errorResponse(400, message);
@@ -454,6 +448,11 @@ async function handleCheckpoint(request: Request): Promise<Response> {
454
448
  ...result,
455
449
  record: stored,
456
450
  }));
451
+ // Mirror into the memory table so search can find it; never throws, capped wait.
452
+ await mirrorCheckpointWithDeadline(() => {
453
+ const { store, embedder } = getComponents();
454
+ return { store, embedder };
455
+ }, stored);
457
456
  return jsonResponse(stored, 201);
458
457
  } catch (err) {
459
458
  const message = err instanceof Error ? err.message : String(err);
@@ -651,6 +650,9 @@ const port = clampInt(envConfig.apiPortRaw(), 4318, 1, 65535);
651
650
 
652
651
  const server = Bun.serve({
653
652
  port,
653
+ // Writes can wait up to 30 s for store-write before they start; Bun's 10 s default would drop
654
+ // the connection while the handler still commits.
655
+ idleTimeout: HTTP_WRITE_IDLE_TIMEOUT_S,
654
656
  hostname: LOCAL_HTTP_HOSTNAME,
655
657
  async fetch(request) {
656
658
  const policy = await enforceLocalHttpRequestPolicy(request);
package/src/asset-sync.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { Embedder } from "./embedder.js";
2
- import type { MemoryStore } from "./store.js";
3
- import type { BriefAsset, MemoryAsset, PinAsset } from "./memory-assets.js";
2
+ import type { MemoryEntry, MemoryStore } from "./store.js";
3
+ import { buildPinAsset, savePinAsset, type BriefAsset, type MemoryAsset, type PinAsset } from "./memory-assets.js";
4
+ import type { RetrievalProfileName } from "./retrieval-profiles.js";
4
5
 
5
6
  function buildPinnedAssetText(asset: PinAsset): string {
6
7
  const tags = asset.tags.join(", ");
@@ -77,3 +78,26 @@ export async function indexAsset(
77
78
  }
78
79
 
79
80
  export const indexPinnedAsset = indexAsset;
81
+
82
+ /**
83
+ * pin_memory(MCP 工具、CLI `pin`、本地 UI `/api/pin` 共用):先把这条记忆的 importance 提到至少 0.95,
84
+ * update() 返回 null(行在查到之后被删掉了)就返回 null——不写 pin 文件、不建索引副本,由调用方报 not found;
85
+ * 写成功才用 update() 返回的那一行生成 pin。2026-10-17 起 update() 不再把被删的行插回来,
86
+ * 原先这里不看返回值,会给一条已经不存在的记忆留下 pin 文件和索引副本。
87
+ */
88
+ export async function pinMemory(
89
+ store: MemoryStore,
90
+ embedder: Embedder,
91
+ entry: MemoryEntry,
92
+ scopeFilter: string[] | undefined,
93
+ options: { title?: string; summary?: string; query?: string; profile?: RetrievalProfileName },
94
+ save: (asset: PinAsset) => string = savePinAsset,
95
+ ): Promise<{ asset: PinAsset; path: string; entry: MemoryEntry } | null> {
96
+ const updated = await store.update(entry.id, { importance: Math.max(entry.importance || 0.7, 0.95) }, scopeFilter);
97
+ if (!updated) return null;
98
+ const score = updated.importance || 0.7;
99
+ const asset = buildPinAsset({ entry: updated, score, sources: { fused: { score } } }, options);
100
+ const path = save(asset);
101
+ await indexAsset(store, embedder, asset);
102
+ return { asset, path, entry: updated };
103
+ }
package/src/audit-log.ts CHANGED
@@ -18,6 +18,8 @@ export type AuditOperation =
18
18
  | "supersede"
19
19
  | "consolidate"
20
20
  | "forget"
21
+ /** A forget that stopped after changing something (see forget-engine.ts). Not a `forget`: the memory may still be there. */
22
+ | "forget_incomplete"
21
23
  | "cascade_forget"
22
24
  | "reject";
23
25
 
package/src/auto-gc.ts CHANGED
@@ -22,7 +22,7 @@ import type { RetentionPolicy } from "./retention-policy.js";
22
22
  import type { AuditLogger } from "./audit-log.js";
23
23
  import * as envConfig from "./env-config.js";
24
24
  import { isUsageSignalActive } from "./usage-tracker.js";
25
- import { acquireLock, releaseLock, getLastDistillTime, stampLock, lockPathForKey } from "./distill-lock.js";
25
+ import { acquireLock, releaseLock, getLastDistillTime, stampLock, clearStamp, lockPathForKey } from "./distill-lock.js";
26
26
  import { logInfo } from "./stderr-log.js";
27
27
 
28
28
  // ---------------------------------------------------------------------------
@@ -315,9 +315,10 @@ export function resetGcTimestamp(): void {
315
315
  lastGcTimestamp = 0;
316
316
  lastVersionGroupRepairTimestamp = 0;
317
317
  try {
318
- releaseLock({ lockPath: lockPathForKey("gc-last") });
318
+ clearStamp({ lockPath: lockPathForKey("gc-last") });
319
+ clearStamp({ lockPath: lockPathForKey("version-group-repair-last") });
320
+ // Only releases gc-run if this process holds it; another holder's lock is never removed.
319
321
  releaseLock({ lockPath: lockPathForKey("gc-run") });
320
- releaseLock({ lockPath: lockPathForKey("version-group-repair-last") });
321
322
  } catch {
322
323
  /* ignore — nothing to clear */
323
324
  }