recallnest 1.4.1 → 2.2.1

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 (233) hide show
  1. package/.env +4 -0
  2. package/CLAUDE.md +7 -3
  3. package/README.md +202 -130
  4. package/README_CN.md +263 -190
  5. package/bin/recallnest.mjs +62 -0
  6. package/config.json +55 -0
  7. package/daily-recall.sh +23 -0
  8. package/integrations/setup-all.sh +31 -0
  9. package/logs/daily-recall.error.log +0 -0
  10. package/logs/daily-recall.log +30 -0
  11. package/logs/ingest-2026-04-11.log +110 -0
  12. package/logs/ingest-2026-04-12.log +90 -0
  13. package/logs/launchagent-stderr.log +0 -0
  14. package/logs/launchagent-stdout.log +0 -0
  15. package/package.json +10 -3
  16. package/src/access-tracker.ts +3 -2
  17. package/src/activity-counter.ts +109 -0
  18. package/src/adaptive-retrieval.ts +6 -5
  19. package/src/admission-control.ts +163 -0
  20. package/src/anchor-generator.ts +80 -0
  21. package/src/api-server.ts +10 -0
  22. package/src/audit-log.ts +122 -0
  23. package/src/auto-gc.ts +98 -29
  24. package/src/auto-recall.ts +33 -4
  25. package/src/capture-engine.ts +372 -23
  26. package/src/cascade-forget.ts +3 -2
  27. package/src/cli.ts +107 -5
  28. package/src/compat.ts +22 -0
  29. package/src/confidence-tracker.ts +210 -47
  30. package/src/conflict-store.ts +12 -3
  31. package/src/consolidation-engine.ts +296 -8
  32. package/src/context-collapse-renderer.ts +222 -0
  33. package/src/context-composer-stable-selection.ts +5 -1
  34. package/src/context-composer.ts +239 -4
  35. package/src/context-reconstructor.ts +404 -0
  36. package/src/conversation-importer.ts +441 -0
  37. package/src/data-checkup.ts +232 -0
  38. package/src/decay-engine.ts +84 -0
  39. package/src/distill-lock.ts +122 -0
  40. package/src/doctor.ts +8 -10
  41. package/src/dream-pipeline.ts +227 -0
  42. package/src/emotion-detector.ts +90 -0
  43. package/src/eval.ts +3 -2
  44. package/src/forget-engine.ts +269 -0
  45. package/src/frequency-tracker.ts +175 -0
  46. package/src/graph-export.ts +644 -0
  47. package/src/ingest.ts +36 -6
  48. package/src/interference-detector.ts +187 -0
  49. package/src/kg-extractor.ts +15 -24
  50. package/src/kg-store.ts +2 -1
  51. package/src/language-hook.ts +67 -0
  52. package/src/llm-client.ts +289 -6
  53. package/src/mcp-server.ts +707 -80
  54. package/src/memory-assets.ts +22 -5
  55. package/src/memory-evolution.ts +365 -0
  56. package/src/memory-lint.ts +429 -0
  57. package/src/memory-output.ts +69 -0
  58. package/src/memory-schema.ts +74 -2
  59. package/src/narrative-schema.ts +79 -0
  60. package/src/narrative-tagger.ts +164 -0
  61. package/src/pii-detector.ts +98 -0
  62. package/src/post-retrieval-filter.ts +76 -0
  63. package/src/prediction-engine.ts +253 -0
  64. package/src/preference-slots.ts +65 -2
  65. package/src/prospective-memory.ts +215 -0
  66. package/src/query-expander.ts +67 -0
  67. package/src/recall-governor.ts +183 -0
  68. package/src/retention-policy.ts +90 -0
  69. package/src/retriever.ts +828 -55
  70. package/src/rif.ts +34 -8
  71. package/src/runtime-config.ts +23 -8
  72. package/src/scope-inventory.ts +4 -3
  73. package/src/scope-policy.ts +4 -0
  74. package/src/session-distiller.ts +307 -0
  75. package/src/session-output.ts +40 -0
  76. package/src/session-schema.ts +37 -0
  77. package/src/session-store.ts +7 -2
  78. package/src/skill-engine.ts +208 -0
  79. package/src/skill-promotion.ts +293 -0
  80. package/src/skill-schema.ts +39 -0
  81. package/src/store.ts +96 -10
  82. package/src/topic-tag.ts +66 -0
  83. package/src/tracker.ts +3 -1
  84. package/src/ui-server.ts +80 -8
  85. package/src/workflow-observation-scope-review.ts +2 -1
  86. package/src/workflow-observation-store.ts +2 -1
  87. package/src/write-verifier.ts +126 -0
  88. package/.claude-plugin/CLAUDE.md +0 -29
  89. package/.claude-plugin/marketplace.json +0 -18
  90. package/.github/workflows/ci.yml +0 -28
  91. package/.mcp.json +0 -9
  92. package/CHANGELOG.md +0 -51
  93. package/DEMO.md +0 -94
  94. package/EVAL.md +0 -89
  95. package/FAILURES.md +0 -75
  96. package/OPERATIONS.md +0 -59
  97. package/ROADMAP.md +0 -198
  98. package/assets/recallnest-workbench.png +0 -0
  99. package/assets/ui/app.js +0 -807
  100. package/assets/ui/index.html +0 -154
  101. package/assets/ui/styles.css +0 -605
  102. package/assets/wechat-qr.jpg +0 -0
  103. package/daily-reflections/2026-03-05.md +0 -15
  104. package/daily-reflections/2026-03-06.md +0 -15
  105. package/daily-reflections/2026-03-07.md +0 -7
  106. package/daily-reflections/2026-03-08.md +0 -34
  107. package/daily-reflections/2026-03-09.md +0 -35
  108. package/daily-reflections/2026-03-10.md +0 -36
  109. package/daily-reflections/2026-03-11.md +0 -35
  110. package/daily-reflections/2026-03-12.md +0 -36
  111. package/daily-reflections/2026-03-13.md +0 -35
  112. package/daily-reflections/2026-03-14.md +0 -35
  113. package/daily-reflections/2026-03-15.md +0 -35
  114. package/daily-reflections/2026-03-16.md +0 -41
  115. package/daily-reflections/2026-03-17.md +0 -10
  116. package/daily-reflections/2026-03-18.md +0 -10
  117. package/daily-reflections/2026-03-19.md +0 -10
  118. package/daily-reflections/2026-03-20.md +0 -10
  119. package/daily-reflections/2026-03-21.md +0 -10
  120. package/daily-reflections/2026-03-22.md +0 -10
  121. package/daily-reflections/2026-03-23.md +0 -10
  122. package/daily-reflections/2026-03-24.md +0 -10
  123. package/daily-reflections/2026-03-25.md +0 -32
  124. package/daily-reflections/2026-03-26.md +0 -37
  125. package/daily-reflections/2026-03-27.md +0 -35
  126. package/daily-reflections/2026-03-28.md +0 -35
  127. package/daily-reflections/2026-03-29.md +0 -35
  128. package/daily-reflections/2026-03-30.md +0 -36
  129. package/daily-reflections/2026-03-31.md +0 -35
  130. package/daily-reflections/2026-04-01.md +0 -35
  131. package/docs/api-reference.md +0 -1291
  132. package/docs/architecture.md +0 -168
  133. package/docs/memory-boundary-contract.md +0 -226
  134. package/docs/memory-categories.md +0 -119
  135. package/docs/pipeline-audit-ultramemory.md +0 -177
  136. package/docs/plans/2026-03-08-query-expansion.md +0 -270
  137. package/docs/plans/2026-03-16-continuity-layer.md +0 -196
  138. package/docs/plans/mcp-tiering-plan.md +0 -126
  139. package/docs/research/recall-quality-audit-2026-03-22.md +0 -153
  140. package/docs/self-evolution.md +0 -198
  141. package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +0 -782
  142. package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +0 -77
  143. package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +0 -95
  144. package/eval/continuity/results-2026-03-16.md +0 -117
  145. package/eval/continuity/results-2026-03-19.md +0 -453
  146. package/eval/continuity/runbook.md +0 -110
  147. package/eval/continuity/seed-cases.md +0 -54
  148. package/eval/reports/2026-03-06-abstract-query-baseline.md +0 -96
  149. package/eval/reports/2026-03-06-aws-ssh-baseline.md +0 -82
  150. package/eval/reports/2026-03-06-baseline.md +0 -82
  151. package/eval/reports/query-expansion.md +0 -138
  152. package/lm +0 -19
  153. package/plan-v1.2-distributable.md +0 -105
  154. package/scripts/archive-distilled-junk.ts +0 -116
  155. package/scripts/backfill-l0l1l2.ts +0 -98
  156. package/scripts/blind-scan.ts +0 -588
  157. package/scripts/cleanup-step1-exact-dups.ts +0 -117
  158. package/scripts/cleanup-step2-template-dups.ts +0 -113
  159. package/scripts/cleanup-step3-stale-content.ts +0 -149
  160. package/scripts/cleanup-step4-orphan-scopes.ts +0 -131
  161. package/scripts/delete-distilled-junk.ts +0 -88
  162. package/scripts/distill-facts.ts +0 -305
  163. package/scripts/health-check.ts +0 -343
  164. package/scripts/incremental-ingest.sh +0 -71
  165. package/scripts/mlp-health-check.ts +0 -382
  166. package/scripts/pull-claude-desktop.ts +0 -515
  167. package/scripts/re-ingest-cc.sh +0 -66
  168. package/scripts/rebalance-memory-health.ts +0 -181
  169. package/scripts/run-desktop-ingest.sh +0 -11
  170. package/scripts/start-server.sh +0 -12
  171. package/scripts/viz-dashboard.html +0 -423
  172. package/scripts/viz-export.ts +0 -703
  173. package/src/__tests__/access-tracker-novelty.test.ts +0 -228
  174. package/src/__tests__/auto-consolidation.test.ts +0 -168
  175. package/src/__tests__/auto-recall.test.ts +0 -131
  176. package/src/__tests__/batch-dedup.test.ts +0 -65
  177. package/src/__tests__/capture-engine.test.ts +0 -1142
  178. package/src/__tests__/capture-heuristic.test.ts +0 -171
  179. package/src/__tests__/cascade-forget.test.ts +0 -210
  180. package/src/__tests__/category-min-scores.test.ts +0 -33
  181. package/src/__tests__/confidence-tracker.test.ts +0 -196
  182. package/src/__tests__/conflict-advisor.test.ts +0 -206
  183. package/src/__tests__/conflict-escalation.test.ts +0 -149
  184. package/src/__tests__/conflict-lifecycle.test.ts +0 -83
  185. package/src/__tests__/conflict-output.test.ts +0 -85
  186. package/src/__tests__/conflict-store.test.ts +0 -374
  187. package/src/__tests__/consolidation-engine.test.ts +0 -181
  188. package/src/__tests__/context-composer-pins.test.ts +0 -64
  189. package/src/__tests__/context-composer-task-selection.test.ts +0 -24
  190. package/src/__tests__/context-renderer.test.ts +0 -91
  191. package/src/__tests__/continuity-eval.test.ts +0 -275
  192. package/src/__tests__/continuity-schema.test.ts +0 -97
  193. package/src/__tests__/core-recall-eval.test.ts +0 -235
  194. package/src/__tests__/core-summary.test.ts +0 -109
  195. package/src/__tests__/dedup-actions.test.ts +0 -58
  196. package/src/__tests__/deterministic-id.test.ts +0 -39
  197. package/src/__tests__/doctor.test.ts +0 -303
  198. package/src/__tests__/drill-down.test.ts +0 -108
  199. package/src/__tests__/embedder.test.ts +0 -122
  200. package/src/__tests__/entity-resolver.test.ts +0 -58
  201. package/src/__tests__/event-segmenter.test.ts +0 -153
  202. package/src/__tests__/hotness-blend.test.ts +0 -76
  203. package/src/__tests__/ingest.test.ts +0 -360
  204. package/src/__tests__/kg-extractor.test.ts +0 -184
  205. package/src/__tests__/kg-store.test.ts +0 -198
  206. package/src/__tests__/llm-consolidation.test.ts +0 -226
  207. package/src/__tests__/memory-boundaries.test.ts +0 -237
  208. package/src/__tests__/memory-health-rebalance.test.ts +0 -144
  209. package/src/__tests__/memory-output.test.ts +0 -215
  210. package/src/__tests__/multi-vector.test.ts +0 -242
  211. package/src/__tests__/ppr-traversal.test.ts +0 -256
  212. package/src/__tests__/preference-guard-smoke.test.ts +0 -211
  213. package/src/__tests__/preference-matcher.test.ts +0 -230
  214. package/src/__tests__/preference-slots.test.ts +0 -106
  215. package/src/__tests__/prospective-memory.test.ts +0 -283
  216. package/src/__tests__/query-entity-detector.test.ts +0 -171
  217. package/src/__tests__/query-expander.test.ts +0 -34
  218. package/src/__tests__/recall-mode.test.ts +0 -64
  219. package/src/__tests__/result-synthesizer.test.ts +0 -198
  220. package/src/__tests__/resume-context.test.ts +0 -4743
  221. package/src/__tests__/retrieval-trace.test.ts +0 -213
  222. package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
  223. package/src/__tests__/retroactive-boost.test.ts +0 -223
  224. package/src/__tests__/scope-inventory.test.ts +0 -200
  225. package/src/__tests__/scope-policy.test.ts +0 -69
  226. package/src/__tests__/session-checkpoint.test.ts +0 -259
  227. package/src/__tests__/session-gc.test.ts +0 -215
  228. package/src/__tests__/store-scope-audit.test.ts +0 -81
  229. package/src/__tests__/temporal-parser.test.ts +0 -241
  230. package/src/__tests__/term-registry.test.ts +0 -121
  231. package/src/__tests__/tool-output-compressor.test.ts +0 -133
  232. package/src/__tests__/version-manager.test.ts +0 -213
  233. package/src/__tests__/workflow-observation.test.ts +0 -400
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Forget Engine — Ethics-aware memory deletion with full cascade.
3
+ *
4
+ * Philosophy: "The right to forget" — memories can be explicitly removed,
5
+ * but the process must be auditable, reversible (via evidence export),
6
+ * and propagated to all derived artifacts (KG triples, pins, evolution chains).
7
+ *
8
+ * Sequence:
9
+ * 1. Fetch target memory
10
+ * 2. Privacy tier check (durable requires explicit confirm)
11
+ * 3. Evidence export (snapshot before deletion for audit trail)
12
+ * 4. KG triple cleanup (via KGStore.deleteBySource)
13
+ * 5. Pin archive (mark related pins as forgotten)
14
+ * 6. Cascade demote (related memories via cascade-forget.ts)
15
+ * 7. Primary delete (remove from LanceDB)
16
+ * 8. Audit log (record the forget operation)
17
+ */
18
+
19
+ import type { MemoryStore, MemoryEntry } from "./store.js";
20
+ import type { KGStore } from "./kg-store.js";
21
+ import type { AuditLogger } from "./audit-log.js";
22
+ import { parsePrivacyTier, type PrivacyTier } from "./memory-schema.js";
23
+ import { parseEvolution, patchEvolution } from "./memory-evolution.js";
24
+ import { cascadeForget, type CascadeForgetConfig, DEFAULT_CASCADE_FORGET_CONFIG } from "./cascade-forget.js";
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Types
28
+ // ---------------------------------------------------------------------------
29
+
30
+ export interface ForgetRequest {
31
+ /** Memory ID to forget (full UUID or 8+ hex prefix) */
32
+ memoryId: string;
33
+ /** Explicit confirmation — required for "durable" tier memories */
34
+ confirm: boolean;
35
+ /** Reason for forgetting (audit trail) */
36
+ reason?: string;
37
+ /** Scope filter for permission check */
38
+ scopeFilter?: string[];
39
+ }
40
+
41
+ export interface ForgetEvidence {
42
+ /** Snapshot of the memory before deletion */
43
+ entry: MemoryEntry;
44
+ /** Privacy tier at time of deletion */
45
+ privacyTier: PrivacyTier;
46
+ /** Evolution chain snapshot */
47
+ evolution: ReturnType<typeof parseEvolution>;
48
+ /** Timestamp of forget operation */
49
+ forgottenAt: string;
50
+ /** Reason provided */
51
+ reason?: string;
52
+ }
53
+
54
+ export interface ForgetResult {
55
+ /** Whether the memory was successfully forgotten */
56
+ success: boolean;
57
+ /** ID of the forgotten memory */
58
+ memoryId: string;
59
+ /** Evidence snapshot (for audit/undo) */
60
+ evidence?: ForgetEvidence;
61
+ /** Number of KG triples removed */
62
+ kgTriplesRemoved: boolean;
63
+ /** Cascade demote results */
64
+ cascadeResult: { demotedCount: number; demotedIds: string[] };
65
+ /** Error message if failed */
66
+ error?: string;
67
+ }
68
+
69
+ export interface ForgetByIdDeps {
70
+ store: MemoryStore;
71
+ kgStore?: KGStore | null;
72
+ auditLogger?: AuditLogger | null;
73
+ cascadeConfig?: CascadeForgetConfig;
74
+ }
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // Core: Forget a single memory
78
+ // ---------------------------------------------------------------------------
79
+
80
+ export async function forgetMemory(
81
+ deps: ForgetByIdDeps,
82
+ request: ForgetRequest,
83
+ ): Promise<ForgetResult> {
84
+ const { store, kgStore, auditLogger, cascadeConfig } = deps;
85
+ const { memoryId, confirm, reason, scopeFilter } = request;
86
+
87
+ // 1. Fetch target
88
+ const entry = await store.get(memoryId, scopeFilter);
89
+ if (!entry) {
90
+ return {
91
+ success: false,
92
+ memoryId,
93
+ kgTriplesRemoved: false,
94
+ cascadeResult: { demotedCount: 0, demotedIds: [] },
95
+ error: `Memory ${memoryId} not found`,
96
+ };
97
+ }
98
+
99
+ // 2. Privacy tier check
100
+ const privacyTier = parsePrivacyTier(entry.metadata);
101
+ if (privacyTier === "durable" && !confirm) {
102
+ return {
103
+ success: false,
104
+ memoryId: entry.id,
105
+ kgTriplesRemoved: false,
106
+ cascadeResult: { demotedCount: 0, demotedIds: [] },
107
+ error: `Memory ${entry.id} has privacy tier "durable" — set confirm=true to proceed`,
108
+ };
109
+ }
110
+
111
+ // 3. Evidence export (snapshot before deletion)
112
+ const evolution = parseEvolution(entry.metadata, entry.timestamp);
113
+ const evidence: ForgetEvidence = {
114
+ entry: { ...entry },
115
+ privacyTier,
116
+ evolution,
117
+ forgottenAt: new Date().toISOString(),
118
+ reason,
119
+ };
120
+
121
+ // 4. KG triple cleanup
122
+ let kgTriplesRemoved = false;
123
+ if (kgStore) {
124
+ try {
125
+ await kgStore.deleteBySource(entry.id);
126
+ kgTriplesRemoved = true;
127
+ } catch (err) {
128
+ console.error("[recallnest] KG cleanup failed during forget:", err instanceof Error ? err.message : String(err));
129
+ }
130
+ }
131
+
132
+ // 5. Cascade demote (related memories get importance reduction)
133
+ let cascadeResult = { demotedCount: 0, demotedIds: [] as string[] };
134
+ try {
135
+ cascadeResult = await cascadeForget(
136
+ store,
137
+ { id: entry.id, vector: entry.vector, scope: entry.scope },
138
+ cascadeConfig ?? DEFAULT_CASCADE_FORGET_CONFIG,
139
+ );
140
+ } catch (err) {
141
+ console.error("[recallnest] Cascade demote failed during forget:", err instanceof Error ? err.message : String(err));
142
+ }
143
+
144
+ // 6. Mark evolution status as "forgotten" before delete (audit breadcrumb)
145
+ try {
146
+ const patchedMetadata = patchEvolution(entry.metadata, {
147
+ status: "archived" as any,
148
+ evolutionNote: `forgotten: ${reason || "user request"}`,
149
+ });
150
+ await store.update(entry.id, { metadata: patchedMetadata }, scopeFilter);
151
+ } catch (err) {
152
+ console.error("[recallnest] Evolution patch failed during forget:", err instanceof Error ? err.message : String(err));
153
+ }
154
+
155
+ // 7. Primary delete
156
+ try {
157
+ await store.delete(entry.id, scopeFilter);
158
+ } catch (err) {
159
+ return {
160
+ success: false,
161
+ memoryId: entry.id,
162
+ evidence,
163
+ kgTriplesRemoved,
164
+ cascadeResult,
165
+ error: `Delete failed: ${err instanceof Error ? err.message : String(err)}`,
166
+ };
167
+ }
168
+
169
+ // 8. Audit log
170
+ try {
171
+ auditLogger?.log({
172
+ operation: "forget",
173
+ scope: entry.scope,
174
+ memoryId: entry.id,
175
+ actor: "system",
176
+ details: `tier=${privacyTier} reason=${reason || "none"} cascade=${cascadeResult.demotedCount}`,
177
+ });
178
+ if (cascadeResult.demotedCount > 0) {
179
+ auditLogger?.log({
180
+ operation: "cascade_forget",
181
+ scope: entry.scope,
182
+ memoryId: entry.id,
183
+ actor: "system",
184
+ details: `demoted ${cascadeResult.demotedCount} related memories: ${cascadeResult.demotedIds.map(id => id.slice(0, 8)).join(",")}`,
185
+ });
186
+ }
187
+ } catch (err) {
188
+ console.error("[recallnest] Audit log failed during forget:", err instanceof Error ? err.message : String(err));
189
+ }
190
+
191
+ return {
192
+ success: true,
193
+ memoryId: entry.id,
194
+ evidence,
195
+ kgTriplesRemoved,
196
+ cascadeResult,
197
+ };
198
+ }
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // Bulk: Forget all memories in a scope
202
+ // ---------------------------------------------------------------------------
203
+
204
+ export interface ForgetByScopeResult {
205
+ forgottenCount: number;
206
+ failedCount: number;
207
+ kgScopeCleared: boolean;
208
+ totalCascadeDemoted: number;
209
+ }
210
+
211
+ export async function forgetByScope(
212
+ deps: ForgetByIdDeps,
213
+ scope: string,
214
+ confirm: boolean,
215
+ reason?: string,
216
+ ): Promise<ForgetByScopeResult> {
217
+ if (!confirm) {
218
+ return { forgottenCount: 0, failedCount: 0, kgScopeCleared: false, totalCascadeDemoted: 0 };
219
+ }
220
+
221
+ const { store, kgStore, auditLogger } = deps;
222
+ let forgottenCount = 0;
223
+ let failedCount = 0;
224
+ let totalCascadeDemoted = 0;
225
+
226
+ // Fetch all entries in scope
227
+ const entries = await store.list([scope], undefined, 5000);
228
+
229
+ for (const entry of entries) {
230
+ const result = await forgetMemory(deps, {
231
+ memoryId: entry.id,
232
+ confirm: true,
233
+ reason: reason || `scope-level forget: ${scope}`,
234
+ scopeFilter: [scope],
235
+ });
236
+
237
+ if (result.success) {
238
+ forgottenCount++;
239
+ totalCascadeDemoted += result.cascadeResult.demotedCount;
240
+ } else {
241
+ failedCount++;
242
+ }
243
+ }
244
+
245
+ // Bulk KG scope cleanup
246
+ let kgScopeCleared = false;
247
+ if (kgStore) {
248
+ try {
249
+ await kgStore.deleteByScope(scope);
250
+ kgScopeCleared = true;
251
+ } catch (err) {
252
+ console.error("[recallnest] KG scope cleanup failed:", err instanceof Error ? err.message : String(err));
253
+ }
254
+ }
255
+
256
+ // Audit the bulk operation
257
+ try {
258
+ auditLogger?.log({
259
+ operation: "forget",
260
+ scope,
261
+ actor: "system",
262
+ details: `scope-forget: ${forgottenCount} deleted, ${failedCount} failed, ${totalCascadeDemoted} cascade-demoted`,
263
+ });
264
+ } catch (err) {
265
+ console.error("[recallnest] Audit log failed during scope forget:", err instanceof Error ? err.message : String(err));
266
+ }
267
+
268
+ return { forgottenCount, failedCount, kgScopeCleared, totalCascadeDemoted };
269
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * P0.2 — Frequency Tracker
3
+ *
4
+ * Tracks how often each memory_id gets retrieved (hit_count).
5
+ * Used to boost frequently accessed memories in scoring.
6
+ *
7
+ * Key design decisions:
8
+ * - Counts per memory_id, not per query text (avoids synonym fragmentation)
9
+ * - Persists to JSON file, loaded on startup
10
+ * - Time decay: 30 days without a hit → effective count halves
11
+ * - Superseded entries do NOT inherit hit_count (fresh starts)
12
+ */
13
+
14
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
15
+ import { dirname, join } from "path";
16
+ import { logWarn } from "./stderr-log.js";
17
+
18
+ // ============================================================================
19
+ // Types
20
+ // ============================================================================
21
+
22
+ export interface FrequencyEntry {
23
+ hitCount: number;
24
+ lastHitAt: number; // ms since epoch
25
+ }
26
+
27
+ export interface FrequencyStats {
28
+ [memoryId: string]: FrequencyEntry;
29
+ }
30
+
31
+ export interface FrequencyTrackerConfig {
32
+ /** Path to persistence file. Default: data/frequency-stats.json */
33
+ filePath: string;
34
+ /** Boost factor per log2(hitCount). Default: 0.15 */
35
+ boostFactor: number;
36
+ /** Days without a hit before effective count halves. Default: 30 */
37
+ decayHalfLifeDays: number;
38
+ /** Minimum hits before boost kicks in. Default: 2 */
39
+ minHitsForBoost: number;
40
+ /** Hit count threshold for auto-promoting to core tier. Default: 3 */
41
+ corePromotionThreshold: number;
42
+ /** Debounce write interval (ms). Default: 10000 */
43
+ flushIntervalMs: number;
44
+ }
45
+
46
+ export const DEFAULT_FREQUENCY_CONFIG: FrequencyTrackerConfig = {
47
+ filePath: join(process.cwd(), "data", "frequency-stats.json"),
48
+ boostFactor: 0.15,
49
+ decayHalfLifeDays: 30,
50
+ minHitsForBoost: 2,
51
+ corePromotionThreshold: 3,
52
+ flushIntervalMs: 10000,
53
+ };
54
+
55
+ // ============================================================================
56
+ // Frequency Tracker
57
+ // ============================================================================
58
+
59
+ export class FrequencyTracker {
60
+ private stats: FrequencyStats = {};
61
+ private dirty = false;
62
+ private flushTimer: ReturnType<typeof setTimeout> | null = null;
63
+ readonly config: FrequencyTrackerConfig;
64
+
65
+ constructor(config?: Partial<FrequencyTrackerConfig>) {
66
+ this.config = { ...DEFAULT_FREQUENCY_CONFIG, ...config };
67
+ this.load();
68
+ }
69
+
70
+ /** Record hits for a set of memory IDs (called after retrieval). */
71
+ recordHits(memoryIds: string[]): void {
72
+ const now = Date.now();
73
+ for (const id of memoryIds) {
74
+ const existing = this.stats[id];
75
+ if (existing) {
76
+ existing.hitCount += 1;
77
+ existing.lastHitAt = now;
78
+ } else {
79
+ this.stats[id] = { hitCount: 1, lastHitAt: now };
80
+ }
81
+ }
82
+ this.dirty = true;
83
+ this.scheduleSave();
84
+ }
85
+
86
+ /**
87
+ * Compute the frequency boost multiplier for a memory.
88
+ * Returns 1.0 (no boost) if below threshold.
89
+ * Formula: 1 + log2(effectiveHits) * boostFactor
90
+ */
91
+ getBoostMultiplier(memoryId: string): number {
92
+ const entry = this.stats[memoryId];
93
+ if (!entry || entry.hitCount < this.config.minHitsForBoost) return 1.0;
94
+
95
+ const effectiveHits = this.effectiveHitCount(entry);
96
+ if (effectiveHits < this.config.minHitsForBoost) return 1.0;
97
+
98
+ return 1 + Math.log2(effectiveHits) * this.config.boostFactor;
99
+ }
100
+
101
+ /** Check if a memory should be auto-promoted to core tier. */
102
+ shouldPromoteToCore(memoryId: string): boolean {
103
+ const entry = this.stats[memoryId];
104
+ if (!entry) return false;
105
+ return this.effectiveHitCount(entry) >= this.config.corePromotionThreshold;
106
+ }
107
+
108
+ /** Get raw stats for a memory (for debugging/testing). */
109
+ getStats(memoryId: string): FrequencyEntry | undefined {
110
+ return this.stats[memoryId];
111
+ }
112
+
113
+ /** Total tracked entries. */
114
+ get size(): number {
115
+ return Object.keys(this.stats).length;
116
+ }
117
+
118
+ /** Force flush to disk (for testing or shutdown). */
119
+ flush(): void {
120
+ if (!this.dirty) return;
121
+ this.save();
122
+ }
123
+
124
+ /** Clean up timer on shutdown. */
125
+ dispose(): void {
126
+ if (this.flushTimer) {
127
+ clearTimeout(this.flushTimer);
128
+ this.flushTimer = null;
129
+ }
130
+ this.flush();
131
+ }
132
+
133
+ // --------------------------------------------------------------------------
134
+ // Internal
135
+ // --------------------------------------------------------------------------
136
+
137
+ private effectiveHitCount(entry: FrequencyEntry): number {
138
+ const daysSinceHit = (Date.now() - entry.lastHitAt) / 86_400_000;
139
+ const decayFactor = Math.exp(
140
+ (-daysSinceHit * Math.LN2) / this.config.decayHalfLifeDays,
141
+ );
142
+ return entry.hitCount * decayFactor;
143
+ }
144
+
145
+ private load(): void {
146
+ try {
147
+ if (existsSync(this.config.filePath)) {
148
+ const raw = readFileSync(this.config.filePath, "utf-8");
149
+ this.stats = JSON.parse(raw) as FrequencyStats;
150
+ }
151
+ } catch {
152
+ logWarn("frequency-tracker: failed to load stats, starting fresh");
153
+ this.stats = {};
154
+ }
155
+ }
156
+
157
+ private save(): void {
158
+ try {
159
+ const dir = dirname(this.config.filePath);
160
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
161
+ writeFileSync(this.config.filePath, JSON.stringify(this.stats, null, 2));
162
+ this.dirty = false;
163
+ } catch (err) {
164
+ logWarn(`frequency-tracker: failed to save stats: ${err}`);
165
+ }
166
+ }
167
+
168
+ private scheduleSave(): void {
169
+ if (this.flushTimer) return;
170
+ this.flushTimer = setTimeout(() => {
171
+ this.flushTimer = null;
172
+ this.save();
173
+ }, this.config.flushIntervalMs);
174
+ }
175
+ }