recallnest 2.3.0 → 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 (258) hide show
  1. package/CHANGELOG.md +187 -0
  2. package/README.md +49 -38
  3. package/README_CN.md +46 -31
  4. package/assets/ui/app.js +1031 -0
  5. package/assets/ui/index.html +224 -0
  6. package/assets/ui/styles.css +781 -0
  7. package/config.json.example +4 -1
  8. package/docs/api-reference.md +1291 -0
  9. package/docs/architecture.md +168 -0
  10. package/docs/connector-spec.md +126 -0
  11. package/eval/cases-canary.json +58 -0
  12. package/eval/cases-scope-robustness.json +278 -0
  13. package/eval/cases.json +117 -20
  14. package/package.json +25 -4
  15. package/src/__tests__/access-tracker-novelty.test.ts +304 -0
  16. package/src/__tests__/activity-counter.test.ts +125 -0
  17. package/src/__tests__/adaptive-fidelity.test.ts +102 -0
  18. package/src/__tests__/admission-control.test.ts +206 -0
  19. package/src/__tests__/anchor-generator.test.ts +67 -0
  20. package/src/__tests__/api-server-security.test.ts +102 -0
  21. package/src/__tests__/auto-consolidation.test.ts +168 -0
  22. package/src/__tests__/auto-recall.test.ts +131 -0
  23. package/src/__tests__/babel-integration.test.ts +144 -0
  24. package/src/__tests__/batch-dedup.test.ts +65 -0
  25. package/src/__tests__/belief-history.test.ts +220 -0
  26. package/src/__tests__/canary-eval.test.ts +114 -0
  27. package/src/__tests__/capture-engine.test.ts +1328 -0
  28. package/src/__tests__/capture-heuristic.test.ts +171 -0
  29. package/src/__tests__/cascade-forget.test.ts +210 -0
  30. package/src/__tests__/category-min-scores.test.ts +116 -0
  31. package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
  32. package/src/__tests__/circuit-breaker.test.ts +99 -0
  33. package/src/__tests__/cluster-consolidation.test.ts +945 -0
  34. package/src/__tests__/confidence-metadata.test.ts +125 -0
  35. package/src/__tests__/confidence-tracker.test.ts +261 -0
  36. package/src/__tests__/conflict-advisor.test.ts +206 -0
  37. package/src/__tests__/conflict-escalation.test.ts +149 -0
  38. package/src/__tests__/conflict-lifecycle.test.ts +83 -0
  39. package/src/__tests__/conflict-output.test.ts +85 -0
  40. package/src/__tests__/conflict-store.test.ts +418 -0
  41. package/src/__tests__/connector-v1.test.ts +181 -0
  42. package/src/__tests__/consolidation-engine.test.ts +430 -0
  43. package/src/__tests__/constructive-retrieval.test.ts +509 -0
  44. package/src/__tests__/context-collapse-renderer.test.ts +214 -0
  45. package/src/__tests__/context-composer-pins.test.ts +64 -0
  46. package/src/__tests__/context-composer-task-selection.test.ts +24 -0
  47. package/src/__tests__/context-reconstructor.test.ts +65 -0
  48. package/src/__tests__/context-renderer.test.ts +91 -0
  49. package/src/__tests__/continuity-eval.test.ts +275 -0
  50. package/src/__tests__/continuity-schema.test.ts +97 -0
  51. package/src/__tests__/contracts.test.ts +262 -0
  52. package/src/__tests__/conversation-importer.test.ts +692 -0
  53. package/src/__tests__/core-recall-eval.test.ts +235 -0
  54. package/src/__tests__/core-summary.test.ts +109 -0
  55. package/src/__tests__/data-checkup.test.ts +276 -0
  56. package/src/__tests__/decay-exemption.test.ts +129 -0
  57. package/src/__tests__/dedup-actions.test.ts +58 -0
  58. package/src/__tests__/dedup-guard.test.ts +39 -0
  59. package/src/__tests__/deterministic-id.test.ts +39 -0
  60. package/src/__tests__/distill-lock.test.ts +305 -0
  61. package/src/__tests__/distill-session-argshape.test.ts +79 -0
  62. package/src/__tests__/doctor.test.ts +319 -0
  63. package/src/__tests__/dream-pipeline.test.ts +780 -0
  64. package/src/__tests__/drill-down.test.ts +108 -0
  65. package/src/__tests__/embedder.test.ts +122 -0
  66. package/src/__tests__/emotion-decay.test.ts +46 -0
  67. package/src/__tests__/emotion-detector.test.ts +56 -0
  68. package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
  69. package/src/__tests__/emotion-retrieval.test.ts +56 -0
  70. package/src/__tests__/entity-resolver.test.ts +58 -0
  71. package/src/__tests__/env-config.test.ts +151 -0
  72. package/src/__tests__/error-signature-boost.test.ts +87 -0
  73. package/src/__tests__/error-signature.test.ts +95 -0
  74. package/src/__tests__/error-taxonomy.test.ts +129 -0
  75. package/src/__tests__/ethics-layer.test.ts +476 -0
  76. package/src/__tests__/event-segmenter.test.ts +153 -0
  77. package/src/__tests__/feature-flags-integration.test.ts +54 -0
  78. package/src/__tests__/frequency-tracker.test.ts +113 -0
  79. package/src/__tests__/freshness.test.ts +186 -0
  80. package/src/__tests__/graph-export.test.ts +489 -0
  81. package/src/__tests__/hotness-blend.test.ts +283 -0
  82. package/src/__tests__/ingest-chunking.test.ts +44 -0
  83. package/src/__tests__/ingest.test.ts +360 -0
  84. package/src/__tests__/interference-detector.test.ts +203 -0
  85. package/src/__tests__/kg-extractor.test.ts +192 -0
  86. package/src/__tests__/kg-store.test.ts +300 -0
  87. package/src/__tests__/llm-client-pivot.test.ts +628 -0
  88. package/src/__tests__/llm-consolidation.test.ts +226 -0
  89. package/src/__tests__/maintenance-locks.test.ts +189 -0
  90. package/src/__tests__/mcp-efficiency.test.ts +288 -0
  91. package/src/__tests__/mcp-related-scopes.test.ts +151 -0
  92. package/src/__tests__/memory-boundaries.test.ts +282 -0
  93. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +345 -0
  94. package/src/__tests__/memory-evolution.test.ts +271 -0
  95. package/src/__tests__/memory-health-rebalance.test.ts +144 -0
  96. package/src/__tests__/memory-lint.test.ts +421 -0
  97. package/src/__tests__/memory-output.test.ts +215 -0
  98. package/src/__tests__/memory-promotion.test.ts +315 -0
  99. package/src/__tests__/migrate-dedup-ids.test.ts +65 -0
  100. package/src/__tests__/multi-vector.test.ts +242 -0
  101. package/src/__tests__/narrative-architecture.test.ts +456 -0
  102. package/src/__tests__/noise-filter.test.ts +205 -0
  103. package/src/__tests__/noise-prototype-bank.test.ts +280 -0
  104. package/src/__tests__/obsidian-connector.test.ts +363 -0
  105. package/src/__tests__/omitted-reporting.test.ts +98 -0
  106. package/src/__tests__/pending-review.test.ts +77 -0
  107. package/src/__tests__/pii-redact.test.ts +333 -0
  108. package/src/__tests__/pivot-apply.test.ts +373 -0
  109. package/src/__tests__/pivot-distill-supervisor.test.ts +457 -0
  110. package/src/__tests__/pivot-distill.test.ts +1178 -0
  111. package/src/__tests__/post-retrieval-filter.test.ts +178 -0
  112. package/src/__tests__/ppr-traversal.test.ts +256 -0
  113. package/src/__tests__/prediction-engine.test.ts +349 -0
  114. package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
  115. package/src/__tests__/preference-guard-smoke.test.ts +211 -0
  116. package/src/__tests__/preference-matcher.test.ts +282 -0
  117. package/src/__tests__/preference-slots.test.ts +205 -0
  118. package/src/__tests__/prospective-memory.test.ts +283 -0
  119. package/src/__tests__/pull-from-macbook-sync-only.test.ts +679 -0
  120. package/src/__tests__/query-entity-detector.test.ts +171 -0
  121. package/src/__tests__/query-expander-alias.test.ts +146 -0
  122. package/src/__tests__/query-expander.test.ts +34 -0
  123. package/src/__tests__/recall-gate.test.ts +266 -0
  124. package/src/__tests__/recall-governor.test.ts +227 -0
  125. package/src/__tests__/recall-mode.test.ts +64 -0
  126. package/src/__tests__/resolve-db-path.test.ts +130 -0
  127. package/src/__tests__/resolve-tier.test.ts +64 -0
  128. package/src/__tests__/result-synthesizer.test.ts +198 -0
  129. package/src/__tests__/resume-context.test.ts +4898 -0
  130. package/src/__tests__/retrieval-eval.test.ts +163 -0
  131. package/src/__tests__/retrieval-trace.test.ts +213 -0
  132. package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
  133. package/src/__tests__/retriever-lazy-init-dispatch.test.ts +123 -0
  134. package/src/__tests__/retroactive-boost.test.ts +223 -0
  135. package/src/__tests__/salience-scoring.test.ts +106 -0
  136. package/src/__tests__/scope-inventory.test.ts +200 -0
  137. package/src/__tests__/scope-policy.test.ts +69 -0
  138. package/src/__tests__/scope-suggester.test.ts +80 -0
  139. package/src/__tests__/session-checkpoint.test.ts +291 -0
  140. package/src/__tests__/session-distiller.test.ts +640 -0
  141. package/src/__tests__/session-gc.test.ts +215 -0
  142. package/src/__tests__/skill-engine.test.ts +552 -0
  143. package/src/__tests__/skill-promotion.test.ts +654 -0
  144. package/src/__tests__/skill-verifier.test.ts +122 -0
  145. package/src/__tests__/source-heartbeat.test.ts +250 -0
  146. package/src/__tests__/stable-context-length-contract.test.ts +62 -0
  147. package/src/__tests__/store-patch-metadata.test.ts +205 -0
  148. package/src/__tests__/store-read-consistency.test.ts +122 -0
  149. package/src/__tests__/store-schema-migration.test.ts +87 -0
  150. package/src/__tests__/store-scope-audit.test.ts +81 -0
  151. package/src/__tests__/store-upsert.test.ts +201 -0
  152. package/src/__tests__/supersede-chain.test.ts +154 -0
  153. package/src/__tests__/temporal-parser.test.ts +241 -0
  154. package/src/__tests__/temporal-validity.test.ts +160 -0
  155. package/src/__tests__/term-registry-terminal-roster.test.ts +127 -0
  156. package/src/__tests__/term-registry.test.ts +121 -0
  157. package/src/__tests__/tool-error-surfacing.test.ts +98 -0
  158. package/src/__tests__/tool-output-compressor.test.ts +160 -0
  159. package/src/__tests__/topic-tag.test.ts +113 -0
  160. package/src/__tests__/trusted-memory.test.ts +839 -0
  161. package/src/__tests__/ui-server-security.test.ts +93 -0
  162. package/src/__tests__/usage-tracker.test.ts +287 -0
  163. package/src/__tests__/version-manager.test.ts +213 -0
  164. package/src/__tests__/workflow-observation-skill-outcome.test.ts +352 -0
  165. package/src/__tests__/workflow-observation.test.ts +433 -0
  166. package/src/__tests__/write-verifier.test.ts +122 -0
  167. package/src/access-tracker.ts +90 -34
  168. package/src/activity-counter.ts +46 -18
  169. package/src/adaptive-retrieval.ts +11 -0
  170. package/src/aliases.ts +133 -0
  171. package/src/api-server.ts +36 -9
  172. package/src/audit-log.ts +4 -2
  173. package/src/auto-consolidation.ts +8 -3
  174. package/src/auto-gc.ts +85 -21
  175. package/src/auto-recall.ts +61 -1
  176. package/src/belief-history.ts +164 -0
  177. package/src/capture-engine.ts +142 -32
  178. package/src/cli.ts +268 -41
  179. package/src/confidence-tracker.ts +36 -18
  180. package/src/conflict-engine.ts +24 -3
  181. package/src/consolidation-engine.ts +255 -23
  182. package/src/context-composer-stable-selection.ts +7 -1
  183. package/src/context-composer.ts +8 -1
  184. package/src/data-checkup.ts +96 -7
  185. package/src/decay-engine.ts +59 -7
  186. package/src/distill-lock.ts +194 -15
  187. package/src/doctor.ts +13 -4
  188. package/src/dream-pipeline.ts +473 -41
  189. package/src/env-config.ts +118 -0
  190. package/src/error-signature.ts +153 -0
  191. package/src/error-taxonomy.ts +115 -0
  192. package/src/eval.ts +402 -14
  193. package/src/freshness.ts +192 -0
  194. package/src/http-request-policy.ts +148 -0
  195. package/src/idempotency.ts +8 -0
  196. package/src/ingest.ts +308 -12
  197. package/src/kg-extractor.ts +13 -3
  198. package/src/kg-store.ts +236 -44
  199. package/src/llm-client.ts +106 -11
  200. package/src/llm-consolidation.ts +2 -1
  201. package/src/mcp-server.ts +87 -1800
  202. package/src/mcp-tool-deps.ts +48 -0
  203. package/src/mcp-tools-advanced.ts +954 -0
  204. package/src/mcp-tools-core.ts +637 -0
  205. package/src/mcp-tools-governance.ts +475 -0
  206. package/src/memory-boundaries.ts +26 -3
  207. package/src/memory-evolution.ts +73 -1
  208. package/src/memory-health-rebalance.ts +4 -0
  209. package/src/memory-lint.ts +183 -10
  210. package/src/memory-output.ts +129 -4
  211. package/src/memory-promotion.ts +246 -0
  212. package/src/memory-schema.ts +25 -2
  213. package/src/memory-store-port.ts +46 -0
  214. package/src/multi-vector.ts +2 -1
  215. package/src/narrative-schema.ts +2 -1
  216. package/src/noise-filter.ts +57 -6
  217. package/src/noise-prototype-bank.ts +408 -0
  218. package/src/pii-detector.ts +207 -5
  219. package/src/pivot-apply.ts +399 -0
  220. package/src/preference-matcher.ts +37 -3
  221. package/src/query-expander.ts +90 -9
  222. package/src/recall-gate.ts +305 -0
  223. package/src/result-synthesizer.ts +2 -1
  224. package/src/retention-policy.ts +2 -1
  225. package/src/retrieval-profiles.ts +46 -4
  226. package/src/retriever.ts +354 -157
  227. package/src/retroactive-boost.ts +6 -2
  228. package/src/runtime-config.ts +78 -6
  229. package/src/scope-suggester.ts +113 -0
  230. package/src/session-distiller.ts +23 -2
  231. package/src/session-engine.ts +2 -1
  232. package/src/session-schema.ts +1 -0
  233. package/src/session-store.ts +5 -0
  234. package/src/skill-engine.ts +78 -0
  235. package/src/skill-promotion.ts +304 -48
  236. package/src/skill-schema.ts +16 -3
  237. package/src/skill-verifier.ts +152 -0
  238. package/src/store.ts +321 -57
  239. package/src/term-registry.ts +35 -0
  240. package/src/tool-output-compressor.ts +3 -1
  241. package/src/ui-server.ts +9 -1
  242. package/src/usage-tracker.ts +242 -0
  243. package/src/version-manager.ts +3 -2
  244. package/src/version.ts +19 -0
  245. package/src/workflow-observation-engine.ts +2 -1
  246. package/src/workflow-observation-schema.ts +2 -1
  247. package/src/workflow-observation-store.ts +6 -2
  248. package/.env +0 -4
  249. package/CLAUDE.md +0 -46
  250. package/config.json +0 -55
  251. package/daily-recall.sh +0 -110
  252. package/logs/daily-recall.error.log +0 -0
  253. package/logs/daily-recall.log +0 -60
  254. package/logs/ingest-2026-04-11.log +0 -110
  255. package/logs/ingest-2026-04-12.log +0 -90
  256. package/logs/ingest-2026-04-13.log +0 -87
  257. package/logs/launchagent-stderr.log +0 -0
  258. package/logs/launchagent-stdout.log +0 -0
@@ -21,6 +21,7 @@
21
21
  */
22
22
 
23
23
  import type { MemoryStore, MemoryEntry } from "./store.js";
24
+ import { isActiveMemory } from "./memory-evolution.js";
24
25
 
25
26
  // ---------------------------------------------------------------------------
26
27
  // Config
@@ -85,12 +86,15 @@ export async function retroactiveBoost(
85
86
  }
86
87
 
87
88
  // Find related entries in the same scope
88
- const candidates = await store.vectorSearch(
89
+ // Superseded belief-history rows come back from vectorSearch too. Boosting one is wasted
90
+ // — they are excluded from retrieval anyway — and it burns maxBoostPerTrigger budget that
91
+ // live entries should be getting.
92
+ const candidates = (await store.vectorSearch(
89
93
  newEntry.vector,
90
94
  config.maxBoostPerTrigger * 3, // fetch more, filter down
91
95
  config.similarityThreshold,
92
96
  [newEntry.scope],
93
- );
97
+ )).filter((candidate) => isActiveMemory(candidate.entry.metadata));
94
98
 
95
99
  const now = Date.now();
96
100
  const minAgeMs = config.minAgeDays * 86_400_000;
@@ -8,9 +8,11 @@ import { createEmbedder, getVectorDimensions, type EmbeddingConfig } from "./emb
8
8
  import { createRetriever, type RetrievalConfig, DEFAULT_RETRIEVAL_CONFIG } from "./retriever.js";
9
9
  import { applyRetrievalProfile } from "./retrieval-profiles.js";
10
10
  import { AccessTracker } from "./access-tracker.js";
11
+ import { createAuditLogger } from "./audit-log.js";
11
12
  import { FrequencyTracker } from "./frequency-tracker.js";
12
13
  import { createLLMClient, type LLMClient, type LLMConfig } from "./llm-client.js";
13
14
  import { logInfo } from "./stderr-log.js";
15
+ import * as envConfig from "./env-config.js";
14
16
 
15
17
  export type RecallMode = "full" | "light" | "summary" | "off";
16
18
 
@@ -33,6 +35,12 @@ export interface LocalMemoryConfig {
33
35
  };
34
36
  sources: Record<string, { path: string; glob: string; description: string }>;
35
37
  retrieval?: Partial<RetrievalConfig>;
38
+ /**
39
+ * Opt-in cross-scope sidecar map for search_memory.
40
+ * Main scoped search remains isolated; callers must pass includeRelatedScopes
41
+ * before configured related scopes are queried and shown separately.
42
+ */
43
+ scopeRelations?: Record<string, string[]>;
36
44
  /**
37
45
  * Default depth for auto-recall injection.
38
46
  * - "full": inject complete text (default, backward compatible)
@@ -60,7 +68,20 @@ export function loadDotEnv(): void {
60
68
  }
61
69
 
62
70
  export function findConfigPath(): string {
63
- if (process.env.LOCAL_MEMORY_CONFIG) return resolve(process.env.LOCAL_MEMORY_CONFIG);
71
+ if (process.env.LOCAL_MEMORY_CONFIG) {
72
+ const explicit = resolve(process.env.LOCAL_MEMORY_CONFIG);
73
+ // Check here rather than letting readFileSync throw a bare ENOENT: scripts
74
+ // resolve the database at module load, so a stale env var would otherwise
75
+ // surface as an unexplained crash before any of their own output appears.
76
+ if (!existsSync(explicit)) {
77
+ throw new Error(
78
+ `LOCAL_MEMORY_CONFIG points to a file that does not exist:\n` +
79
+ ` ${explicit}\n` +
80
+ ` Fix: correct the env var, or unset it to use the repo's config.json.`
81
+ );
82
+ }
83
+ return explicit;
84
+ }
64
85
 
65
86
  const localConfig = resolve(metaDir(import.meta), "../config.json");
66
87
  if (existsSync(localConfig)) return localConfig;
@@ -96,8 +117,35 @@ export function expandHome(p: string): string {
96
117
  return p;
97
118
  }
98
119
 
120
+ /**
121
+ * Single source of truth for "which LanceDB directory do we operate on".
122
+ *
123
+ * Every entry point — MCP server, CLI, and maintenance scripts — must resolve
124
+ * the database through this function. Scripts that hardcode "./data/lancedb"
125
+ * or read a non-existent config key silently operate on a *different* database
126
+ * than the server does, so a health check, cleanup, or backfill can report on
127
+ * an empty directory while the real store sits elsewhere.
128
+ *
129
+ * Relative dbPath values resolve against the repo root (not process.cwd()), so
130
+ * a script's behaviour does not depend on where it was invoked from.
131
+ *
132
+ * Pass an already-loaded config where you have one. Calling this with no
133
+ * argument reads config.json from disk — convenient at a script's top level,
134
+ * but it means a missing or misconfigured file surfaces here, before the
135
+ * script's own output. findConfigPath() raises an actionable error for that.
136
+ */
137
+ export function resolveDbPath(config?: LocalMemoryConfig): string {
138
+ const cfg = config ?? loadConfig();
139
+ return resolve(metaDir(import.meta), "..", expandHome(cfg.dbPath));
140
+ }
141
+
142
+ /** Sidecar directory (frequency-stats.json etc.) that lives beside the database. */
143
+ export function resolveDataDir(config?: LocalMemoryConfig): string {
144
+ return resolve(resolveDbPath(config), "..");
145
+ }
146
+
99
147
  export function createComponents(config: LocalMemoryConfig, profileName?: string) {
100
- const dbPath = resolve(metaDir(import.meta), "..", expandHome(config.dbPath));
148
+ const dbPath = resolveDbPath(config);
101
149
  validateStoragePath(dbPath);
102
150
 
103
151
  const embeddingConfig: EmbeddingConfig = {
@@ -112,24 +160,48 @@ export function createComponents(config: LocalMemoryConfig, profileName?: string
112
160
 
113
161
  const embedder = createEmbedder(embeddingConfig);
114
162
  const store = new MemoryStore({ dbPath, vectorDim: embedder.dimensions });
115
- const baseRetrievalConfig = {
163
+ const mergedRetrievalConfig = {
116
164
  ...DEFAULT_RETRIEVAL_CONFIG,
117
165
  ...(config.retrieval || {}),
118
166
  };
167
+ // rerankApiKey 与 embedding.apiKey 同款支持 ${ENV_VAR} 引用。此前 retrieval 段是整体
168
+ // 展开、不过 resolveEnv,导致这个字段只能写明文——等于逼配置文件存凭证,是 rerank
169
+ // 一直没被启用的障碍之一。
170
+ const baseRetrievalConfig = mergedRetrievalConfig.rerankApiKey
171
+ ? { ...mergedRetrievalConfig, rerankApiKey: resolveEnv(mergedRetrievalConfig.rerankApiKey) }
172
+ : mergedRetrievalConfig;
119
173
  const { profile, config: retrieverConfig } = applyRetrievalProfile(baseRetrievalConfig, profileName);
120
174
  const retriever = createRetriever(store, embedder, retrieverConfig);
121
175
 
122
176
  // Attach access tracker for reinforcement-based decay
123
177
  const accessTracker = new AccessTracker(store);
178
+ accessTracker.registerExitFlush();
124
179
  retriever.setAccessTracker(accessTracker);
125
180
 
126
181
  // P0.2: Attach frequency tracker for hit-count based boosting
127
- const dataDir = resolve(metaDir(import.meta), "..", expandHome(config.dbPath), "..");
182
+ const dataDir = resolveDataDir(config);
128
183
  const frequencyTracker = new FrequencyTracker({
129
184
  filePath: join(dataDir, "frequency-stats.json"),
130
185
  });
131
186
  retriever.setFrequencyTracker(frequencyTracker);
132
187
 
188
+ // F-1: 补接 audit logger,让 retrieve 操作真的被记录。
189
+ //
190
+ // 2026-08-12 修复:`setAuditLogger`(retriever.ts:749)自定义以来**全仓零调用者**,
191
+ // 而 retriever.ts:897 的 `this.auditLogger?.log({ operation: "retrieve", ... })`
192
+ // 早就写好了 —— 可选链让它永远静默 no-op。结果是 audit-log.ts 文件头注释声称
193
+ // "record every store/update/delete/retrieve operation"、AuditOperation 类型里
194
+ // 也列着 "retrieve",而生产 audit.jsonl 里 2841 条记录**零条 retrieve**。
195
+ //
196
+ // 同族的 setAccessTracker / setFrequencyTracker 就在上面两行,都接了,唯独这个漏了。
197
+ // 与同日修的 dream 末尾 resetWriteCount 漏传 scope 完全同型:接线漏一处、运行时
198
+ // 不报错、静默失效 —— 差别只在那个是漏参数、这个是漏调用。
199
+ //
200
+ // simplified: audit.jsonl 无轮转机制(修复时 644K / 2841 行,约 230 字节一行)。
201
+ // 接上 retrieve 后增速会明显上升,因为检索远比写入频繁。
202
+ // 升级触发条件:文件超过约 50MB 时加按月轮转,别等到它拖慢 append。
203
+ retriever.setAuditLogger(createAuditLogger(join(dataDir, "audit.jsonl")));
204
+
133
205
  // Create LLM client if configured (optional, graceful)
134
206
  let llm: LLMClient | null = null;
135
207
  if (config.llm) {
@@ -143,7 +215,7 @@ export function createComponents(config: LocalMemoryConfig, profileName?: string
143
215
  }
144
216
 
145
217
  export function createStoreOnly(config: LocalMemoryConfig): MemoryStore {
146
- const dbPath = resolve(metaDir(import.meta), "..", expandHome(config.dbPath));
218
+ const dbPath = resolveDbPath(config);
147
219
  validateStoragePath(dbPath);
148
220
  return new MemoryStore({
149
221
  dbPath,
@@ -179,7 +251,7 @@ export function resolveRecallMode(config: LocalMemoryConfig, perCallOverride?: s
179
251
  if (perCallOverride && VALID_RECALL_MODES.includes(perCallOverride as RecallMode)) {
180
252
  return perCallOverride as RecallMode;
181
253
  }
182
- const envMode = process.env.RECALLNEST_RECALL_MODE;
254
+ const envMode = envConfig.recallModeRaw();
183
255
  if (envMode && VALID_RECALL_MODES.includes(envMode as RecallMode)) {
184
256
  return envMode as RecallMode;
185
257
  }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * P1-A: scope 拼写漂移提示(FAILURES.md 5-13 retrieval gap)。
3
+ *
4
+ * matchesScopeFilter 走精确/前缀匹配,拼错或惯例不一致的 scope(`recallnst`、
5
+ * `cc_55bcbfb3`)会静默返回 0 hits,使用者无从知道是"真没有"还是"scope 写错"。
6
+ * 本模块在 0-hit 且显式传了 scope 时给出相近 scope 提示。
7
+ *
8
+ * 只提示、不自动 fallback 重查——自动改写会静默改变查询语义,agent/用户
9
+ * 看到提示后自行决定。
10
+ */
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Pure matching
14
+ // ---------------------------------------------------------------------------
15
+
16
+ /** 归一化:小写 + 吃掉 -/_/空格(惯例差异:`cc-foo` vs `cc_foo` vs `ccfoo`)。 */
17
+ export function normalizeScope(s: string): string {
18
+ return s.toLowerCase().replace(/[-_\s]+/g, "");
19
+ }
20
+
21
+ /** Two-row Levenshtein with early exit — 距离超过 max 直接返回 max+1。 */
22
+ export function boundedLevenshtein(a: string, b: string, max: number): number {
23
+ if (Math.abs(a.length - b.length) > max) return max + 1;
24
+ if (a === b) return 0;
25
+
26
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
27
+ let curr = new Array<number>(b.length + 1);
28
+
29
+ for (let i = 1; i <= a.length; i++) {
30
+ curr[0] = i;
31
+ let rowMin = curr[0];
32
+ for (let j = 1; j <= b.length; j++) {
33
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
34
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
35
+ if (curr[j] < rowMin) rowMin = curr[j];
36
+ }
37
+ if (rowMin > max) return max + 1;
38
+ [prev, curr] = [curr, prev];
39
+ }
40
+ return prev[b.length];
41
+ }
42
+
43
+ /**
44
+ * 对输入 scope 给出相近的已知 scope。
45
+ * 两级:① normalize 后精确相等 ② 前缀包含(双向)或编辑距离 ≤ 2。
46
+ * 候选应同时包含完整 scope 与其 prefix 家族(`cc:55bc...` 的 `cc`)——
47
+ * 拼错的通常是短 prefix,对长 session scope 做编辑距离没有意义。
48
+ */
49
+ export function suggestScopes(input: string, knownScopes: string[], maxSuggestions = 3): string[] {
50
+ const norm = normalizeScope(input);
51
+ if (norm.length === 0) return [];
52
+
53
+ const exact: string[] = [];
54
+ const close: Array<{ scope: string; rank: number }> = [];
55
+
56
+ for (const known of knownScopes) {
57
+ const kn = normalizeScope(known);
58
+ if (kn === norm) {
59
+ exact.push(known);
60
+ continue;
61
+ }
62
+ if (kn.startsWith(norm) || norm.startsWith(kn)) {
63
+ close.push({ scope: known, rank: 0.5 });
64
+ continue;
65
+ }
66
+ const d = boundedLevenshtein(norm, kn, 2);
67
+ if (d <= 2) close.push({ scope: known, rank: d });
68
+ }
69
+
70
+ if (exact.length > 0) return exact.slice(0, maxSuggestions);
71
+ return close
72
+ .sort((a, b) => a.rank - b.rank || a.scope.length - b.scope.length)
73
+ .map(c => c.scope)
74
+ .slice(0, maxSuggestions);
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Cached provider
79
+ // ---------------------------------------------------------------------------
80
+
81
+ /**
82
+ * 带 60s 缓存的建议器工厂——known scopes 来自 store.stats().scopeCounts,
83
+ * 全表聚合不便宜,0-hit 提示是低频路径,短缓存足够新鲜。
84
+ * 候选 = prefix 家族(冒号前段,去重) + 完整 scope 串。
85
+ */
86
+ export function createScopeSuggester(
87
+ getScopeCounts: () => Promise<Record<string, number>>,
88
+ ttlMs = 60_000,
89
+ ): (input: string) => Promise<string[]> {
90
+ let cache: { at: number; candidates: string[] } | null = null;
91
+
92
+ return async function suggest(input: string): Promise<string[]> {
93
+ const now = Date.now();
94
+ if (!cache || now - cache.at > ttlMs) {
95
+ const counts = await getScopeCounts();
96
+ const keys = Object.keys(counts);
97
+ const prefixes = new Set<string>();
98
+ for (const k of keys) {
99
+ const i = k.indexOf(":");
100
+ if (i > 0) prefixes.add(k.slice(0, i));
101
+ }
102
+ // prefix 家族在前:作为查询 scope 它们最常被手敲、也最常被拼错
103
+ cache = { at: now, candidates: [...prefixes, ...keys] };
104
+ }
105
+ return suggestScopes(input, cache.candidates);
106
+ };
107
+ }
108
+
109
+ /** 0-hit 提示行(空建议返回空串,caller 原样拼接即可)。 */
110
+ export function formatScopeSuggestion(inputScope: string, suggestions: string[]): string {
111
+ if (suggestions.length === 0) return "";
112
+ return `⚠️ scope '${inputScope}' 命中 0 条,相近 scope: [${suggestions.join(", ")}]`;
113
+ }
@@ -7,6 +7,8 @@ import type { AuditLogger } from "./audit-log.js";
7
7
  import type { StoredMemoryRecord, DurableMemoryCategory } from "./memory-schema.js";
8
8
  import { persistMemory } from "./capture-engine.js";
9
9
  import { detectLang, getSessionPromptHook } from "./language-hook.js";
10
+ import { redactSecrets } from "./pii-detector.js";
11
+ import { withWriteLock } from "./distill-lock.js";
10
12
 
11
13
  // ============================================================================
12
14
  // Types
@@ -71,6 +73,7 @@ const DEFAULT_DIMENSION_LABELS: Record<string, string> = {
71
73
  errors_and_fixes: "Errors and fix records",
72
74
  problem_solving: "Problem solving process",
73
75
  user_quotes: "User original quotes preserved",
76
+ user_profile: "Stable facts about the user: identity, role, background, long-term context",
74
77
  unfinished_tasks: "Unfinished tasks",
75
78
  current_state: "Current work state",
76
79
  next_steps: "Suggested next steps",
@@ -93,6 +96,15 @@ const DIMENSION_TO_MEMORY: Record<string, DimensionMapping> = {
93
96
  errors_and_fixes: { category: "cases", importance: 0.7 },
94
97
  problem_solving: { category: "patterns", importance: 0.8 },
95
98
  user_quotes: { category: "preferences", importance: 0.7 },
99
+ // user_profile → profile:让 distill 能沉淀"用户是谁"的稳定身份事实(不只是偏好)。
100
+ // distill 走 source=agent,不被 resolveIngestBoundary 降级,直接进 durable profile。
101
+ user_profile: { category: "profile", importance: 0.8 },
102
+ // 2026-05-13 P1.2: 加 unfinished_tasks + next_steps 持久化映射
103
+ // LLM 早在 DEFAULT_DIMENSION_LABELS 行 74/76 已产出这两条,但缺映射被丢弃
104
+ // 都映射到 cases/patterns 走 tag 兜底(extractAndPersist 自动加 tags: ["session_distill", dimKey])
105
+ // 未来如要 unresolved-first 召回,按 tags 含 "unfinished_tasks" 过滤即可
106
+ unfinished_tasks: { category: "cases", importance: 0.75 },
107
+ next_steps: { category: "patterns", importance: 0.65 },
96
108
  };
97
109
 
98
110
  // ============================================================================
@@ -158,8 +170,10 @@ export async function summarizeSession(
158
170
  messages: ConversationMessage[],
159
171
  llm: LLMClient,
160
172
  ): Promise<SummaryResult> {
173
+ // F-3b: Redact per-message BEFORE the 500-char cut — truncating first could
174
+ // slice a secret in half and leak its prefix past the redaction regexes.
161
175
  const conversationText = messages
162
- .map((m) => `[${m.role}${m.tool_name ? `:${m.tool_name}` : ""}] ${m.content.slice(0, 500)}`)
176
+ .map((m) => `[${m.role}${m.tool_name ? `:${m.tool_name}` : ""}] ${redactSecrets(m.content).text.slice(0, 500)}`)
163
177
  .join("\n")
164
178
  .slice(0, 8000);
165
179
 
@@ -295,7 +309,14 @@ export async function distillSession(
295
309
  };
296
310
 
297
311
  if (shouldPersist && Object.keys(summaryResult.dimensions).length > 0) {
298
- persistResult = await extractAndPersist(summaryResult, deps, scope);
312
+ // P0-1: serialize the persist phase per scope across processes. Wait (not skip) —
313
+ // skipping would drop this session's distilled knowledge when two sessions distill
314
+ // the same scope at once. Writes are fast and store-write already makes each write
315
+ // idempotent, so the brief wait is lossless; microcompact/summary above run unlocked.
316
+ persistResult = await withWriteLock(
317
+ `distill-${scope}`,
318
+ () => extractAndPersist(summaryResult, deps, scope),
319
+ );
299
320
  }
300
321
 
301
322
  return {
@@ -6,6 +6,7 @@ import {
6
6
  type SessionCheckpointRecord,
7
7
  SessionCheckpointRecordSchema,
8
8
  } from "./session-schema.js";
9
+ import { buildIdempotentRecordId } from "./idempotency.js";
9
10
 
10
11
  const REPO_STATE_PATTERN = /\bgit status\b|未提交|staged|uncommitted|已修改文件|modified files?|untracked|新增文件|dirty repo|dirty worktree/iu;
11
12
  const SENTENCE_SPLIT_PATTERN = /(?:\r?\n)+|(?<=[。!?!?;;])\s+/u;
@@ -141,7 +142,7 @@ export function buildSessionCheckpointResult(rawInput: unknown): SessionCheckpoi
141
142
 
142
143
  const record = SessionCheckpointRecordSchema.parse({
143
144
  ...sanitizedInput,
144
- checkpointId: randomUUID(),
145
+ checkpointId: buildIdempotentRecordId("chk", sanitizedInput.idempotencyKey) ?? randomUUID(),
145
146
  resolvedScope: resolveCheckpointScope(sanitizedInput),
146
147
  });
147
148
 
@@ -14,6 +14,7 @@ export const SessionCheckpointInputSchema = z.object({
14
14
  nextActions: normalizedStringListSchema("nextActions", 6, 200),
15
15
  entities: normalizedStringListSchema("entities", 8, 120),
16
16
  files: normalizedStringListSchema("files", 12, 220),
17
+ idempotencyKey: optionalBoundedStringSchema(160),
17
18
  updatedAt: z.string().datetime().default(() => new Date().toISOString()),
18
19
  });
19
20
 
@@ -78,6 +78,11 @@ export class SessionCheckpointStore {
78
78
  const timestampToken = parsed.updatedAt.replace(/[:.]/g, "-");
79
79
  const path = join(this.dataDir, `${timestampToken}-${parsed.checkpointId}.json`);
80
80
  try {
81
+ const suffix = `-${parsed.checkpointId}.json`;
82
+ for (const name of readdirSync(this.dataDir).filter((item) => item.endsWith(suffix))) {
83
+ const existingPath = join(this.dataDir, name);
84
+ if (existingPath !== path) unlinkSync(existingPath);
85
+ }
81
86
  writeFileSync(path, JSON.stringify(parsed, null, 2) + "\n");
82
87
  } catch (err) {
83
88
  console.error("[recallnest] Failed to save session checkpoint:", err instanceof Error ? err.message : String(err));
@@ -177,6 +177,84 @@ export async function persistSkill(
177
177
  };
178
178
  }
179
179
 
180
+ type OutcomeDeps = Pick<MemoryStore, "getById" | "update">;
181
+
182
+ export type SkillOutcomeUpdate =
183
+ | { updated: true; successCount: number; failureCount: number; lastRefinedAt: string }
184
+ | { updated: false; reason: "skill_not_found" | "not_a_skill" | "metadata_parse_error" | "skill_metadata_missing" | "store_update_failed" };
185
+
186
+ /**
187
+ * Record a skill usage outcome by incrementing successCount/failureCount on the skill's metadata.
188
+ *
189
+ * Mapping: `success` → successCount +1; everything else (`failure`/`corrected`/`missed`) → failureCount +1.
190
+ * Returns `{ updated: false, reason }` for missing/corrupt skills without throwing — callers can log and continue.
191
+ */
192
+ export async function recordSkillOutcome(
193
+ store: OutcomeDeps,
194
+ skillId: string,
195
+ outcome: "success" | "failure" | "corrected" | "missed",
196
+ ): Promise<SkillOutcomeUpdate> {
197
+ const entry = await store.getById(skillId);
198
+ if (!entry) {
199
+ return { updated: false, reason: "skill_not_found" };
200
+ }
201
+
202
+ if (entry.category !== SKILL_CATEGORY) {
203
+ return { updated: false, reason: "not_a_skill" };
204
+ }
205
+
206
+ let meta: Record<string, unknown>;
207
+ try {
208
+ meta = JSON.parse(entry.metadata || "{}");
209
+ } catch {
210
+ return { updated: false, reason: "metadata_parse_error" };
211
+ }
212
+
213
+ const skill = meta.skill;
214
+ if (!skill || typeof skill !== "object") {
215
+ return { updated: false, reason: "skill_metadata_missing" };
216
+ }
217
+
218
+ const s = skill as Record<string, unknown>;
219
+ const currentSuccess = typeof s.successCount === "number" ? s.successCount : 0;
220
+ const currentFailure = typeof s.failureCount === "number" ? s.failureCount : 0;
221
+
222
+ const nextSuccessCount = outcome === "success" ? currentSuccess + 1 : currentSuccess;
223
+ const nextFailureCount = outcome === "success" ? currentFailure : currentFailure + 1;
224
+ const nextLastRefinedAt = new Date().toISOString();
225
+
226
+ const nextSkill = {
227
+ ...s,
228
+ successCount: nextSuccessCount,
229
+ failureCount: nextFailureCount,
230
+ lastRefinedAt: nextLastRefinedAt,
231
+ };
232
+
233
+ const updatedMetadata = JSON.stringify({
234
+ ...meta,
235
+ skill: nextSkill,
236
+ });
237
+
238
+ // Use entry.id (full UUID resolved by getById's prefix lookup) for update,
239
+ // not skillId (caller-provided, may be 8+ hex prefix). This avoids ambiguous-prefix
240
+ // mutation risk: if a prefix matched here, update would also need to do prefix
241
+ // resolution, doubling the chance of writing to the wrong entry.
242
+ const updated = await store.update(entry.id, {
243
+ metadata: updatedMetadata,
244
+ });
245
+
246
+ if (!updated) {
247
+ return { updated: false, reason: "store_update_failed" };
248
+ }
249
+
250
+ return {
251
+ updated: true,
252
+ successCount: nextSuccessCount,
253
+ failureCount: nextFailureCount,
254
+ lastRefinedAt: nextLastRefinedAt,
255
+ };
256
+ }
257
+
180
258
  export async function retrieveSkills(
181
259
  store: SearchDeps,
182
260
  embedder: EmbedDeps,