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
@@ -38,10 +38,18 @@ export const DEFAULT_ACCESS_TRACKER_CONFIG: AccessTrackerConfig = {
38
38
  reinforcementFactor: 0.5,
39
39
  maxMultiplier: 3.0,
40
40
  accessFreshnessHalfLifeDays: 30,
41
- noveltyThreshold: 0.35,
41
+ // 0 = novelty gate off. 生产实测(2026-07-23):0.35 要求命中的 cosine ≤0.65 才打点,
42
+ // 真实相关命中几乎都 >0.65,导致 accessCount 在生产库全空——"最被用到"的记忆
43
+ // 恰好全被排除,方向与"用进废退"相反。防刷由 cooldown 承担;要恢复旧行为显式传 0.35。
44
+ noveltyThreshold: 0,
42
45
  cooldownMs: 300_000, // 5 minutes
43
46
  };
44
47
 
48
+ /** readerIds 在 metadata 里的存储上限。distinctReaderCount 到达上限后饱和不再涨。 */
49
+ // simplified: capped array + saturating count instead of Artel's exact memory_reads
50
+ // ledger table — promotion thresholds are ≤4 distinct readers, so saturation at 8 is harmless.
51
+ export const READER_ID_CAP = 8;
52
+
45
53
  // ============================================================================
46
54
  // Core
47
55
  // ============================================================================
@@ -52,11 +60,36 @@ export class AccessTracker {
52
60
  private flushPromise: Promise<void> | null = null;
53
61
  /** Cooldown map: entryId → last reinforced timestamp */
54
62
  private cooldownMap = new Map<string, number>();
63
+ /** Distinct-reader identity: one stdio MCP server process ≈ one CC session.
64
+ * Used to maintain readerIds / distinctReaderCount on flush (Artel-style read fan-out). */
65
+ readonly readerId: string;
66
+ private exitFlushRegistered = false;
55
67
 
56
68
  constructor(
57
69
  private store: MemoryStore,
58
70
  private config: AccessTrackerConfig = DEFAULT_ACCESS_TRACKER_CONFIG,
59
- ) {}
71
+ readerId?: string,
72
+ ) {
73
+ this.readerId = readerId ?? `r-${crypto.randomUUID().slice(0, 8)}`;
74
+ }
75
+
76
+ /**
77
+ * Best-effort flush on normal process exit (stdin close → clean shutdown).
78
+ * Covers the "retrieval happened <flushIntervalMs before exit" window where the
79
+ * debounce timer would otherwise drop pending deltas. Opt-in so tests creating
80
+ * many trackers don't leak process listeners. Not wired to SIGTERM on purpose —
81
+ * an async handler there isn't guaranteed to complete and we don't want to own
82
+ * the server's exit semantics.
83
+ */
84
+ registerExitFlush(): void {
85
+ if (this.exitFlushRegistered) return;
86
+ this.exitFlushRegistered = true;
87
+ process.once("beforeExit", () => {
88
+ this.flush().catch(err => {
89
+ logWarn("AccessTracker exit flush failed:", err);
90
+ });
91
+ });
92
+ }
60
93
 
61
94
  /**
62
95
  * Determine whether a retrieval result should trigger access reinforcement.
@@ -163,43 +196,66 @@ export class AccessTracker {
163
196
 
164
197
  for (const [id, delta] of batch) {
165
198
  try {
166
- const entry = await this.store.getById(id);
167
- if (!entry) continue;
168
-
169
- const meta = safeParseMetadata(entry.metadata);
170
- const prevCount = typeof meta.accessCount === "number" ? meta.accessCount : 0;
171
- const newCount = prevCount + delta;
172
- const importance = entry.importance ?? 0.6;
173
- const lastAccessedAt = now;
174
-
175
- // Evaluate tier change
176
- const currentTier = resolveTier(entry.metadata);
177
- const newTier = evaluateTierChange(
178
- currentTier,
179
- newCount,
180
- importance,
181
- lastAccessedAt,
182
- );
183
-
184
- const tierChanged = newTier !== currentTier;
185
- if (tierChanged) {
186
- if (TIER_ORDER[newTier] > TIER_ORDER[currentTier]) {
199
+ // Read-modify-write goes through the store's per-id patch queue so a
200
+ // concurrent usage/confidence patch can't overwrite this delta (or
201
+ // vice versa). patchFn stays pure — logging happens after settle.
202
+ let tierChange: { from: MemoryTier; to: MemoryTier; newCount: number } | null = null;
203
+ const result = await this.store.patchMetadata(id, (meta, entry) => {
204
+ const prevCount = typeof meta.accessCount === "number" ? meta.accessCount : 0;
205
+ const newCount = prevCount + delta;
206
+ const importance = entry.importance ?? 0.6;
207
+ const lastAccessedAt = now;
208
+
209
+ const currentTier = resolveTier(entry.metadata, importance);
210
+ const newTier = evaluateTierChange(
211
+ currentTier,
212
+ newCount,
213
+ importance,
214
+ lastAccessedAt,
215
+ );
216
+ tierChange = newTier !== currentTier
217
+ ? { from: currentTier, to: newTier, newCount }
218
+ : null;
219
+
220
+ // Distinct-reader bookkeeping (Artel read fan-out, lightweight form):
221
+ // dedup by readerId in a capped array; count saturates at cap (under-
222
+ // counting is safer than over-counting for promotion gating).
223
+ const rawReaders = Array.isArray(meta.readerIds)
224
+ ? (meta.readerIds as unknown[]).filter((x): x is string => typeof x === "string")
225
+ : [];
226
+ let readerIds = rawReaders;
227
+ let distinctReaderCount = typeof meta.distinctReaderCount === "number"
228
+ ? meta.distinctReaderCount
229
+ : rawReaders.length;
230
+ if (!rawReaders.includes(this.readerId) && rawReaders.length < READER_ID_CAP) {
231
+ readerIds = [...rawReaders, this.readerId];
232
+ distinctReaderCount = readerIds.length;
233
+ }
234
+
235
+ return {
236
+ ...meta,
237
+ accessCount: newCount,
238
+ lastAccessedAt,
239
+ readerIds,
240
+ distinctReaderCount,
241
+ ...(tierChange ? { tier: tierChange.to } : {}),
242
+ };
243
+ });
244
+
245
+ // Entry vanished mid-flight → drop the delta (same as the old
246
+ // getById-null path). A patch failure throws and is re-queued below.
247
+ if (!result) continue;
248
+
249
+ if (tierChange !== null) {
250
+ const change: { from: MemoryTier; to: MemoryTier; newCount: number } = tierChange;
251
+ if (TIER_ORDER[change.to] > TIER_ORDER[change.from]) {
187
252
  promotions++;
188
- logInfo(`[INFO] Tier promotion: ${id.slice(0, 8)} ${currentTier} → ${newTier} (access=${newCount})`);
253
+ logInfo(`[INFO] Tier promotion: ${id.slice(0, 8)} ${change.from} → ${change.to} (access=${change.newCount})`);
189
254
  } else {
190
255
  demotions++;
191
- logInfo(`[INFO] Tier demotion: ${id.slice(0, 8)} ${currentTier} → ${newTier}`);
256
+ logInfo(`[INFO] Tier demotion: ${id.slice(0, 8)} ${change.from} → ${change.to}`);
192
257
  }
193
258
  }
194
-
195
- const updatedMeta = {
196
- ...meta,
197
- accessCount: newCount,
198
- lastAccessedAt,
199
- ...(tierChanged ? { tier: newTier } : {}),
200
- };
201
-
202
- await this.store.update(id, { metadata: JSON.stringify(updatedMeta) });
203
259
  } catch {
204
260
  // Re-queue failed entries for next flush
205
261
  this.pending.set(id, (this.pending.get(id) || 0) + delta);
@@ -11,6 +11,7 @@ import {
11
11
  mkdirSync,
12
12
  } from "node:fs";
13
13
  import { join, dirname } from "node:path";
14
+ import * as envConfig from "./env-config.js";
14
15
 
15
16
  // ---------------------------------------------------------------------------
16
17
  // Config
@@ -29,7 +30,7 @@ export interface ActivityCounterConfig {
29
30
 
30
31
  export const DEFAULT_ACTIVITY_CONFIG: ActivityCounterConfig = {
31
32
  statsPath: join(
32
- process.env.RECALLNEST_DATA_DIR || "data",
33
+ envConfig.dataDir(),
33
34
  "activity-stats.json",
34
35
  ),
35
36
  lightThreshold: 15,
@@ -44,8 +45,8 @@ export type DistillTier = "none" | "light" | "standard" | "deep";
44
45
  // ---------------------------------------------------------------------------
45
46
 
46
47
  interface ActivityStats {
47
- writesSinceLastDistill: number;
48
- lastResetAt: number;
48
+ /** Per-scope write counts since that scope's last dream/distill reset. */
49
+ scopes: Record<string, number>;
49
50
  }
50
51
 
51
52
  function resolveConfig(
@@ -56,12 +57,24 @@ function resolveConfig(
56
57
 
57
58
  function readStats(statsPath: string): ActivityStats {
58
59
  if (!existsSync(statsPath)) {
59
- return { writesSinceLastDistill: 0, lastResetAt: Date.now() };
60
+ return { scopes: {} };
60
61
  }
61
62
  try {
62
- return JSON.parse(readFileSync(statsPath, "utf-8"));
63
+ const parsed: unknown = JSON.parse(readFileSync(statsPath, "utf-8"));
64
+ // Robust against the legacy global format ({writesSinceLastDistill}) — no migration,
65
+ // just treat anything without a `scopes` map as empty (counts restart per scope).
66
+ if (
67
+ parsed !== null &&
68
+ typeof parsed === "object" &&
69
+ "scopes" in parsed &&
70
+ typeof (parsed as { scopes?: unknown }).scopes === "object" &&
71
+ (parsed as { scopes?: unknown }).scopes !== null
72
+ ) {
73
+ return { scopes: (parsed as { scopes: Record<string, number> }).scopes };
74
+ }
75
+ return { scopes: {} };
63
76
  } catch {
64
- return { writesSinceLastDistill: 0, lastResetAt: Date.now() };
77
+ return { scopes: {} };
65
78
  }
66
79
  }
67
80
 
@@ -74,34 +87,49 @@ function writeStats(statsPath: string, stats: ActivityStats): void {
74
87
  // Public API
75
88
  // ---------------------------------------------------------------------------
76
89
 
77
- /** Increment write counter by n (default 1). Returns new count. */
90
+ /** Increment a scope's write counter by n (default 1). Returns the scope's new count. */
78
91
  export function incrementWriteCount(
92
+ scope: string,
79
93
  n = 1,
80
94
  cfg?: Partial<ActivityCounterConfig>,
81
95
  ): number {
82
96
  const { statsPath } = resolveConfig(cfg);
83
97
  const stats = readStats(statsPath);
84
- stats.writesSinceLastDistill += n;
98
+ stats.scopes[scope] = (stats.scopes[scope] ?? 0) + n;
85
99
  writeStats(statsPath, stats);
86
- return stats.writesSinceLastDistill;
100
+ return stats.scopes[scope];
87
101
  }
88
102
 
89
- /** Read current write count without modifying. */
90
- export function getWriteCount(cfg?: Partial<ActivityCounterConfig>): number {
103
+ /** Read a scope's current write count without modifying (0 if never written). */
104
+ export function getWriteCount(scope: string, cfg?: Partial<ActivityCounterConfig>): number {
91
105
  const { statsPath } = resolveConfig(cfg);
92
- return readStats(statsPath).writesSinceLastDistill;
106
+ return readStats(statsPath).scopes[scope] ?? 0;
93
107
  }
94
108
 
95
- /** Reset counter after successful distill. */
96
- export function resetWriteCount(cfg?: Partial<ActivityCounterConfig>): void {
109
+ /** Reset one scope's counter after its successful dream/distill (leaves other scopes untouched). */
110
+ export function resetWriteCount(scope: string, cfg?: Partial<ActivityCounterConfig>): void {
97
111
  const { statsPath } = resolveConfig(cfg);
98
- writeStats(statsPath, { writesSinceLastDistill: 0, lastResetAt: Date.now() });
112
+ const stats = readStats(statsPath);
113
+ delete stats.scopes[scope];
114
+ writeStats(statsPath, stats);
115
+ }
116
+
117
+ /** Scopes whose write count is at or above `threshold` — the dream scheduler's work list. */
118
+ export function listScopesAboveThreshold(
119
+ threshold: number,
120
+ cfg?: Partial<ActivityCounterConfig>,
121
+ ): string[] {
122
+ const { statsPath } = resolveConfig(cfg);
123
+ const stats = readStats(statsPath);
124
+ return Object.entries(stats.scopes)
125
+ .filter(([, count]) => count >= threshold)
126
+ .map(([scope]) => scope);
99
127
  }
100
128
 
101
- /** Determine which distill tier the current write count warrants. */
102
- export function getDistillTier(cfg?: Partial<ActivityCounterConfig>): DistillTier {
129
+ /** Determine which distill tier a scope's write count warrants. */
130
+ export function getDistillTier(scope: string, cfg?: Partial<ActivityCounterConfig>): DistillTier {
103
131
  const config = resolveConfig(cfg);
104
- const count = getWriteCount(cfg);
132
+ const count = getWriteCount(scope, cfg);
105
133
  if (count >= config.deepThreshold) return "deep";
106
134
  if (count >= config.standardThreshold) return "standard";
107
135
  if (count >= config.lightThreshold) return "light";
@@ -55,8 +55,19 @@ export function shouldSkipRetrieval(query: string, minLength?: number): boolean
55
55
 
56
56
  const effectiveMinLength = minLength ?? (hasCJK ? 2 : 15);
57
57
 
58
+ // 【本地分叉点 · 2026-08-08 · 勿在同步上游时覆盖回去】
59
+ // 单 token(不含空格)= 实体查找意图,与问号一样豁免长度闸。
60
+ // 上游的 ASCII 阈值 15 是按「英文自然语句」定的(挡 "hello there" 这类闲聊),
61
+ // 但项目名/工具名几乎都短于 15:cobbler=7 / slidesmith=10 / hippo-wiki=10 / recallnest=10,
62
+ // 于是「甩一个名字过去」这种最自然的查法一律返回空——库里明明有。
63
+ // 红绿实测:`cobbler` → No results;`cobbler?` → 100% 命中 12 条,只差一个问号豁免。
64
+ // 中文不受影响(阈值 2),所以该缺陷只在英文实体名上暴露,一直没被发现。
65
+ // 安全性:本条排在 SKIP_PATTERNS 之后,hi / ok / ping / test / ls / git 等单词仍被挡住。
66
+ const isSingleToken = !/\s/.test(trimmed);
67
+
58
68
  // Short non-question messages are skipped; questions (? ?) are always worth checking
59
69
  if (trimmed.length < effectiveMinLength &&
70
+ !isSingleToken &&
60
71
  !trimmed.includes('?') && !trimmed.includes('?')) {
61
72
  return true;
62
73
  }
package/src/aliases.ts ADDED
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Alias dictionary for query rewriting.
3
+ *
4
+ * Manually maintained — do NOT use LLM expansion (risks precision pollution).
5
+ * Each rule appends entity tokens to the query when the pattern matches,
6
+ * giving BM25/vector search more anchor points without losing the original wording.
7
+ *
8
+ * Why expand (append) rather than replace:
9
+ * - Preserves the user's exact wording for vector embedding fidelity.
10
+ * - BM25 gets extra keyword anchors to find the right entity.
11
+ * - Reversible via inspecting the expanded vs. original query.
12
+ *
13
+ * Add new rules sparingly. Each entry should solve a real failure
14
+ * documented in FAILURES.md, not a hypothetical.
15
+ */
16
+
17
+ export interface AliasRule {
18
+ pattern: RegExp;
19
+ expansion: string;
20
+ comment?: string;
21
+ }
22
+
23
+ export const ALIAS_RULES: AliasRule[] = [
24
+ // Entity aliases — colloquial → canonical name
25
+ {
26
+ pattern: /(?:我的)?记忆项目/,
27
+ expansion: "recallnest",
28
+ comment: "FAILURES 2026-04-25: '我的记忆项目' → recallnest",
29
+ },
30
+ {
31
+ pattern: /我的桥/,
32
+ expansion: "telegram-ai-bridge",
33
+ comment: "Alice 用'我的桥'指 telegram-ai-bridge",
34
+ },
35
+
36
+ // Operations vocabulary — operator wording → canonical entities
37
+ {
38
+ pattern: /aws\s*(?:bot|ssh|podcast)/i,
39
+ expansion: "aws-bot hermes-aws aws-podcast-daily",
40
+ comment: "FAILURES 2026-03-06: aws bot/ssh/podcast → aws-bot hermes-aws entities",
41
+ },
42
+ {
43
+ pattern: /bot\s*(?:挂|崩|不能用|跑不起|失联|down)/i,
44
+ expansion: "bot-doctor hermes docker launchd 排查 重启",
45
+ comment: "Operator bot-crash debugging vocabulary",
46
+ },
47
+
48
+ // Cross-window / continuity hints — these strengthen recall of session state.
49
+ // Pattern 必须明确出现"隔壁/另一窗口/session"才扩;纯"之前说过"太宽会污染
50
+ // 普通话题(如 fuzzy_ai_feelings 的 "之前聊过 AI 感受" 被错误归类成 cross-window)。
51
+ {
52
+ pattern: /(?:刚刚|刚才|之前)(?:.{0,10})(?:隔壁|另一个|其他)(?:.{0,10})(?:窗口|session)/,
53
+ expansion: "checkpoint session resume_context 跨窗口",
54
+ comment: "FAILURES 2026-05-10: cross-window real-time sync hint",
55
+ },
56
+
57
+ // Tool provenance — common ambiguous tool references
58
+ {
59
+ pattern: /taobao.{0,5}(?:mcp|官方)/i,
60
+ expansion: "MCP server provenance source",
61
+ comment: "FAILURES 2026-05-01: taobao MCP official-vs-third-party",
62
+ },
63
+
64
+ // RecallNest self-introspection — capability queries
65
+ {
66
+ pattern: /recallnest.{0,15}(?:有哪些|工具|能做|功能|tool)/i,
67
+ expansion: "search_memory store_memory resume_context checkpoint_session MCP",
68
+ comment: "FAILURES 2026-04-20: RN self-introspection capability digest",
69
+ },
70
+ {
71
+ pattern: /recallnest.{0,10}(?:ingest|跑了|跑过|状态|telemetry)/i,
72
+ expansion: "import_conversations weekly-distill ingest telemetry 成功 失败",
73
+ comment: "FAILURES 2026-04-20: RN ingest telemetry queries",
74
+ },
75
+
76
+ // Personal-life recall — subscription / friends provenance
77
+ {
78
+ pattern: /(?:chatgpt|订阅).{0,10}(?:谁|怎么|哪里|帮|顶)/i,
79
+ expansion: "ChatGPT Plus 群友 加拿大 堂姐 澳洲 代订 aliceljyalice",
80
+ comment: "FAILURES 2026-05-12: ChatGPT subscription provenance chain",
81
+ },
82
+ ];
83
+
84
+ /** 超过此长度的 query 不做别名扩展——长 query 是粘贴文档场景,扩展只添噪声 (P1-B)。 */
85
+ export const ALIAS_QUERY_MAX_LENGTH = 100;
86
+
87
+ /**
88
+ * Expand a query by appending alias tokens for matching patterns.
89
+ * Returns the original query unchanged if no rules match.
90
+ *
91
+ * 注意分工:本模块只承载**静态 builtin 规则**,作用于 retrieve() 全通道
92
+ * (vector + BM25)。**用户级** alias(data/alias-map.json)由
93
+ * query-expander.ts 加载,只作用于 BM25 通道(hybridRetrieval 内部)——
94
+ * 用户别名不进 vector embedding,避免随手加的别名造成语义向量漂移。
95
+ *
96
+ * Example:
97
+ * expandQueryWithAliases("帮我看我的记忆项目")
98
+ * → "帮我看我的记忆项目 recallnest"
99
+ * expandQueryWithAliases("aws ssh 怎么连")
100
+ * → "aws ssh 怎么连 aws-bot hermes-aws aws-podcast-daily"
101
+ */
102
+ export function expandQueryWithAliases(query: string): string {
103
+ if (!query || typeof query !== "string") return query;
104
+ if (query.length > ALIAS_QUERY_MAX_LENGTH) return query;
105
+
106
+ const additions: string[] = [];
107
+ const seen = new Set<string>();
108
+
109
+ for (const rule of ALIAS_RULES) {
110
+ if (rule.pattern.test(query)) {
111
+ for (const token of rule.expansion.split(/\s+/)) {
112
+ if (token && !seen.has(token) && !query.includes(token)) {
113
+ additions.push(token);
114
+ seen.add(token);
115
+ }
116
+ }
117
+ }
118
+ }
119
+
120
+ return additions.length > 0 ? `${query} ${additions.join(" ")}` : query;
121
+ }
122
+
123
+ /**
124
+ * Inspection helper — return which builtin rules matched a query, for debugging.
125
+ * 用户级规则(BM25-only)的检视走 query-expander.ts 的 explainUserAliases。
126
+ */
127
+ export function explainAliasExpansion(query: string): Array<{ pattern: string; expansion: string; comment?: string }> {
128
+ return ALIAS_RULES.filter((rule) => rule.pattern.test(query)).map((rule) => ({
129
+ pattern: rule.pattern.source,
130
+ expansion: rule.expansion,
131
+ comment: rule.comment,
132
+ }));
133
+ }
package/src/api-server.ts CHANGED
@@ -17,19 +17,25 @@ import { ConflictStatusSchema } from "./conflict-schema.js";
17
17
  import { resolveConflictCandidate } from "./conflict-engine.js";
18
18
  import { escalateConflicts } from "./conflict-escalation.js";
19
19
  import { ConflictCandidateStore } from "./conflict-store.js";
20
+ import { createAuditLogger } from "./audit-log.js";
20
21
  import { parseConflictAttention, summarizeConflictLifecycle } from "./conflict-lifecycle.js";
21
22
  import { buildConflictAuditSummary, clusterConflicts, summarizeConflictAdvice } from "./conflict-advisor.js";
22
23
  import { buildWorkflowEvidence, buildWorkflowObservationRecord, inspectWorkflowDashboard, inspectWorkflowHealth } from "./workflow-observation-engine.js";
24
+ import { recordSkillOutcome } from "./skill-engine.js";
23
25
  import { WorkflowObservationStore } from "./workflow-observation-store.js";
24
26
  import { buildManagedCheckpointObservation, buildManagedResumeObservation } from "./workflow-observation-managed.js";
25
27
  import { runAutoRecall } from "./auto-recall.js";
26
28
  import { buildRetrievalContext, resolveScopeSelection } from "./scope-policy.js";
27
29
  import { runMemoryLint } from "./memory-lint.js";
30
+ import { enforceLocalHttpRequestPolicy, LOCAL_HTTP_HOSTNAME } from "./http-request-policy.js";
31
+ import * as envConfig from "./env-config.js";
32
+ import { PACKAGE_VERSION } from "./version.js";
28
33
 
29
34
  const config = (loadDotEnv(), loadConfig());
30
35
  const getComponents = createComponentResolver(config);
31
36
  const checkpointStore = new SessionCheckpointStore();
32
37
  const conflictStore = new ConflictCandidateStore();
38
+ const auditLogger = createAuditLogger();
33
39
  const workflowObservationStore = new WorkflowObservationStore();
34
40
 
35
41
  // ============================================================================
@@ -235,7 +241,7 @@ async function handleStore(request: Request): Promise<Response> {
235
241
  const body = await readJson(request);
236
242
  try {
237
243
  const { store, embedder } = getComponents();
238
- const stored = await persistMemory({ store, embedder, conflictStore }, body);
244
+ const stored = await persistMemory({ store, embedder, conflictStore, auditLogger }, body);
239
245
 
240
246
  return jsonResponse({
241
247
  id: stored.id,
@@ -284,7 +290,7 @@ async function handlePattern(request: Request): Promise<Response> {
284
290
  const body = await readJson(request);
285
291
  try {
286
292
  const { store, embedder } = getComponents();
287
- const stored = await persistWorkflowPattern({ store, embedder, conflictStore }, body);
293
+ const stored = await persistWorkflowPattern({ store, embedder, conflictStore, auditLogger }, body);
288
294
  return jsonResponse({
289
295
  id: stored.id,
290
296
  stored: stored.disposition !== "conflict",
@@ -308,7 +314,7 @@ async function handleCase(request: Request): Promise<Response> {
308
314
  const body = await readJson(request);
309
315
  try {
310
316
  const { store, embedder } = getComponents();
311
- const stored = await persistCaseMemory({ store, embedder, conflictStore }, body);
317
+ const stored = await persistCaseMemory({ store, embedder, conflictStore, auditLogger }, body);
312
318
  return jsonResponse({
313
319
  id: stored.id,
314
320
  stored: stored.disposition !== "conflict",
@@ -332,7 +338,7 @@ async function handlePromote(request: Request): Promise<Response> {
332
338
  const body = await readJson(request);
333
339
  try {
334
340
  const { store, embedder } = getComponents();
335
- const stored = await promoteMemory({ store, embedder, conflictStore }, body);
341
+ const stored = await promoteMemory({ store, embedder, conflictStore, auditLogger }, body);
336
342
  return jsonResponse({
337
343
  id: stored.id,
338
344
  stored: stored.disposition !== "conflict",
@@ -455,13 +461,29 @@ async function handleCheckpoint(request: Request): Promise<Response> {
455
461
  }
456
462
  }
457
463
 
458
- /** POST /v1/workflow-observe — persist a workflow observation outside durable memory */
464
+ /** POST /v1/workflow-observe — persist a workflow observation outside durable memory; bumps skill counters when `skillId` is present */
459
465
  async function handleWorkflowObserve(request: Request): Promise<Response> {
460
466
  const body = await readJson(request);
461
467
  try {
462
468
  const record = buildWorkflowObservationRecord(body);
463
469
  const stored = await workflowObservationStore.save(record);
464
- return jsonResponse(stored, 201);
470
+
471
+ let skillOutcome: unknown = undefined;
472
+ if (record.skillId) {
473
+ try {
474
+ const { store } = getComponents();
475
+ const result = await recordSkillOutcome(store, record.skillId, record.outcome);
476
+ skillOutcome = result;
477
+ if (!result.updated) {
478
+ console.warn(`[api /v1/workflow-observe] skillId=${record.skillId} update skipped: ${result.reason}`);
479
+ }
480
+ } catch (err) {
481
+ console.warn(`[api /v1/workflow-observe] recordSkillOutcome threw for skillId=${record.skillId}:`, err);
482
+ skillOutcome = { updated: false, reason: "exception" };
483
+ }
484
+ }
485
+
486
+ return jsonResponse({ observation: stored, ...(skillOutcome !== undefined ? { skillOutcome } : {}) }, 201);
465
487
  } catch (err) {
466
488
  const message = err instanceof Error ? err.message : String(err);
467
489
  return errorResponse(400, message);
@@ -605,7 +627,7 @@ async function handleHealth(): Promise<Response> {
605
627
  const stats = await store.stats();
606
628
  return jsonResponse({
607
629
  status: "ok",
608
- version: "1.0.0",
630
+ version: PACKAGE_VERSION,
609
631
  totalMemories: stats.totalCount,
610
632
  timestamp: new Date().toISOString(),
611
633
  });
@@ -625,12 +647,17 @@ async function handleHealth(): Promise<Response> {
625
647
  // Server
626
648
  // ============================================================================
627
649
 
628
- const port = clampInt(process.env.RECALLNEST_API_PORT, 4318, 1, 65535);
650
+ const port = clampInt(envConfig.apiPortRaw(), 4318, 1, 65535);
629
651
 
630
652
  const server = Bun.serve({
631
653
  port,
632
- hostname: "127.0.0.1",
654
+ hostname: LOCAL_HTTP_HOSTNAME,
633
655
  async fetch(request) {
656
+ const policy = await enforceLocalHttpRequestPolicy(request);
657
+ if (!policy.allowed) {
658
+ return errorResponse(policy.status, policy.message);
659
+ }
660
+
634
661
  const url = new URL(request.url);
635
662
  const { pathname } = url;
636
663
  const method = request.method;
package/src/audit-log.ts CHANGED
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
9
9
  import { dirname, join } from "node:path";
10
+ import * as envConfig from "./env-config.js";
10
11
 
11
12
  export type AuditOperation =
12
13
  | "store"
@@ -17,7 +18,8 @@ export type AuditOperation =
17
18
  | "supersede"
18
19
  | "consolidate"
19
20
  | "forget"
20
- | "cascade_forget";
21
+ | "cascade_forget"
22
+ | "reject";
21
23
 
22
24
  export interface AuditEntry {
23
25
  timestamp: string; // ISO 8601
@@ -59,7 +61,7 @@ function parseLines(raw: string): AuditEntry[] {
59
61
  export function createAuditLogger(logPath?: string): AuditLogger {
60
62
  const resolvedPath =
61
63
  logPath ??
62
- join(process.env.RECALLNEST_DATA_DIR || "data", "audit.jsonl");
64
+ join(envConfig.dataDir(), "audit.jsonl");
63
65
 
64
66
  // Ensure directory exists (silent on failure)
65
67
  try {
@@ -10,7 +10,8 @@
10
10
  * reason is operational hygiene.
11
11
  */
12
12
 
13
- import type { MemoryStore } from "./store.js";
13
+ import type { MemoryEntry } from "./store.js";
14
+ import type { MemoryStorePort } from "./memory-store-port.js";
14
15
  import { ConsolidationEngine, type ConsolidationConfig, type ConsolidationResult, DEFAULT_CONSOLIDATION_CONFIG } from "./consolidation-engine.js";
15
16
  import type { LLMClient } from "./llm-client.js";
16
17
  import { isLLMConsolidationEnabled, evaluateCluster, executeMergeDecisions } from "./llm-consolidation.js";
@@ -54,16 +55,20 @@ export interface AutoConsolidationResult {
54
55
  /**
55
56
  * Check conditions and run consolidation if thresholds are met.
56
57
  * Returns immediately if conditions not met (no-op).
58
+ *
59
+ * @deprecated Prefer dream-pipeline.ts for full maintenance. This module is
60
+ * kept for compatibility with callers that only need count/time-gated
61
+ * consolidation.
57
62
  */
58
63
  export async function maybeConsolidate(
59
- store: MemoryStore,
64
+ store: MemoryStorePort,
60
65
  scope: string,
61
66
  config: AutoConsolidationConfig = DEFAULT_AUTO_CONSOLIDATION_CONFIG,
62
67
  /** Tier 3.7: Optional LLM client for semantic consolidation decisions */
63
68
  llm?: LLMClient | null,
64
69
  ): Promise<AutoConsolidationResult> {
65
70
  const stats = await store.stats([scope]);
66
- const currentCount = stats.total ?? 0;
71
+ const currentCount = stats.totalCount ?? 0;
67
72
 
68
73
  // Condition 1: enough new memories since last run
69
74
  const newSinceLastRun = currentCount - lastRunMemoryCount;