recallnest 2.2.0 → 2.3.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 (249) hide show
  1. package/.env +4 -0
  2. package/CLAUDE.md +1 -1
  3. package/README.md +30 -5
  4. package/README_CN.md +30 -5
  5. package/config.json +55 -0
  6. package/connectors/examples/email-adapter.ts +73 -0
  7. package/connectors/examples/logs-adapter.ts +103 -0
  8. package/connectors/examples/rss-adapter.ts +95 -0
  9. package/logs/daily-recall.error.log +0 -0
  10. package/logs/daily-recall.log +60 -0
  11. package/logs/ingest-2026-04-11.log +110 -0
  12. package/logs/ingest-2026-04-12.log +90 -0
  13. package/logs/ingest-2026-04-13.log +87 -0
  14. package/logs/launchagent-stderr.log +0 -0
  15. package/logs/launchagent-stdout.log +0 -0
  16. package/package.json +1 -1
  17. package/src/cli.ts +30 -0
  18. package/src/connector-types.ts +57 -0
  19. package/src/conversation-importer.ts +25 -1
  20. package/src/data-checkup.ts +31 -1
  21. package/src/doctor.ts +29 -0
  22. package/src/ingest.ts +203 -0
  23. package/src/mcp-server.ts +2 -2
  24. package/src/obsidian-connector.ts +282 -0
  25. package/src/session-schema.ts +2 -2
  26. package/src/source-heartbeat.ts +108 -0
  27. package/.claude-plugin/CLAUDE.md +0 -29
  28. package/.claude-plugin/marketplace.json +0 -18
  29. package/.dockerignore +0 -11
  30. package/.github/workflows/ci.yml +0 -28
  31. package/.mcp.json +0 -9
  32. package/CHANGELOG.md +0 -51
  33. package/DEMO.md +0 -94
  34. package/Dockerfile +0 -20
  35. package/EVAL.md +0 -89
  36. package/FAILURES.md +0 -75
  37. package/OPERATIONS.md +0 -59
  38. package/ROADMAP.md +0 -245
  39. package/SKILL.md +0 -250
  40. package/assets/dashboard.png +0 -0
  41. package/assets/knowledge-graph.png +0 -0
  42. package/assets/recallnest-workbench.png +0 -0
  43. package/assets/screenshots/ui-full.png +0 -0
  44. package/assets/screenshots/ui-overview.png +0 -0
  45. package/assets/screenshots/ui-search.png +0 -0
  46. package/assets/ui/app.js +0 -1031
  47. package/assets/ui/index.html +0 -224
  48. package/assets/ui/styles.css +0 -781
  49. package/assets/wechat-qr.jpg +0 -0
  50. package/bun.lock +0 -337
  51. package/daily-reflections/2026-03-05.md +0 -15
  52. package/daily-reflections/2026-03-06.md +0 -15
  53. package/daily-reflections/2026-03-07.md +0 -7
  54. package/daily-reflections/2026-03-08.md +0 -34
  55. package/daily-reflections/2026-03-09.md +0 -35
  56. package/daily-reflections/2026-03-10.md +0 -36
  57. package/daily-reflections/2026-03-11.md +0 -35
  58. package/daily-reflections/2026-03-12.md +0 -36
  59. package/daily-reflections/2026-03-13.md +0 -35
  60. package/daily-reflections/2026-03-14.md +0 -35
  61. package/daily-reflections/2026-03-15.md +0 -35
  62. package/daily-reflections/2026-03-16.md +0 -41
  63. package/daily-reflections/2026-03-17.md +0 -10
  64. package/daily-reflections/2026-03-18.md +0 -10
  65. package/daily-reflections/2026-03-19.md +0 -10
  66. package/daily-reflections/2026-03-20.md +0 -10
  67. package/daily-reflections/2026-03-21.md +0 -10
  68. package/daily-reflections/2026-03-22.md +0 -10
  69. package/daily-reflections/2026-03-23.md +0 -10
  70. package/daily-reflections/2026-03-24.md +0 -10
  71. package/daily-reflections/2026-03-25.md +0 -32
  72. package/daily-reflections/2026-03-26.md +0 -37
  73. package/daily-reflections/2026-03-27.md +0 -35
  74. package/daily-reflections/2026-03-28.md +0 -35
  75. package/daily-reflections/2026-03-29.md +0 -35
  76. package/daily-reflections/2026-03-30.md +0 -36
  77. package/daily-reflections/2026-03-31.md +0 -35
  78. package/daily-reflections/2026-04-01.md +0 -35
  79. package/daily-reflections/2026-04-02.md +0 -35
  80. package/daily-reflections/2026-04-03.md +0 -37
  81. package/daily-reflections/2026-04-04.md +0 -37
  82. package/daily-reflections/2026-04-05.md +0 -35
  83. package/daily-reflections/2026-04-06.md +0 -36
  84. package/daily-reflections/2026-04-07.md +0 -35
  85. package/daily-reflections/2026-04-08.md +0 -35
  86. package/daily-reflections/2026-04-12.md +0 -35
  87. package/data/alias-map.example.json +0 -22
  88. package/docs/api-reference.md +0 -1291
  89. package/docs/architecture.md +0 -168
  90. package/docs/memory-boundary-contract.md +0 -226
  91. package/docs/memory-categories.md +0 -119
  92. package/docs/pipeline-audit-ultramemory.md +0 -177
  93. package/docs/plans/2026-03-08-query-expansion.md +0 -270
  94. package/docs/plans/2026-03-16-continuity-layer.md +0 -196
  95. package/docs/plans/2026-04-01-recall-boost.md +0 -100
  96. package/docs/plans/mcp-tiering-plan.md +0 -126
  97. package/docs/research/recall-quality-audit-2026-03-22.md +0 -153
  98. package/docs/self-evolution.md +0 -198
  99. package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +0 -782
  100. package/docs/superpowers/plans/2026-04-11-constructive-retrieval-emotion.md +0 -1195
  101. package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +0 -77
  102. package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +0 -95
  103. package/docs/superpowers/specs/2026-04-11-constructive-retrieval-emotion-design.md +0 -398
  104. package/eval/continuity/results-2026-03-16.md +0 -117
  105. package/eval/continuity/results-2026-03-19.md +0 -453
  106. package/eval/continuity/runbook.md +0 -110
  107. package/eval/continuity/seed-cases.md +0 -54
  108. package/eval/reports/2026-03-06-abstract-query-baseline.md +0 -96
  109. package/eval/reports/2026-03-06-aws-ssh-baseline.md +0 -82
  110. package/eval/reports/2026-03-06-baseline.md +0 -82
  111. package/eval/reports/query-expansion.md +0 -138
  112. package/glama.json +0 -6
  113. package/lm +0 -19
  114. package/plan-philosophy-upgrades.md +0 -363
  115. package/plan-product-upgrades.md +0 -155
  116. package/plan-v1.2-distributable.md +0 -105
  117. package/plan.md +0 -140
  118. package/research-memory-evolution.md +0 -440
  119. package/research-rl-agent-memory.md +0 -330
  120. package/scripts/archive-distilled-junk.ts +0 -116
  121. package/scripts/backfill-anchors.ts +0 -130
  122. package/scripts/backfill-l0l1l2.ts +0 -98
  123. package/scripts/blind-scan.ts +0 -588
  124. package/scripts/cleanup-step1-exact-dups.ts +0 -117
  125. package/scripts/cleanup-step2-template-dups.ts +0 -113
  126. package/scripts/cleanup-step3-stale-content.ts +0 -149
  127. package/scripts/cleanup-step4-orphan-scopes.ts +0 -131
  128. package/scripts/delete-distilled-junk.ts +0 -88
  129. package/scripts/diagnose-vector-search.ts +0 -132
  130. package/scripts/distill-facts.ts +0 -305
  131. package/scripts/health-check.ts +0 -343
  132. package/scripts/incremental-ingest.sh +0 -71
  133. package/scripts/migrate-language.ts +0 -75
  134. package/scripts/mlp-health-check.ts +0 -382
  135. package/scripts/pull-claude-desktop.ts +0 -515
  136. package/scripts/re-ingest-cc.sh +0 -66
  137. package/scripts/rebalance-memory-health.ts +0 -181
  138. package/scripts/run-desktop-ingest.sh +0 -11
  139. package/scripts/start-server.sh +0 -12
  140. package/scripts/verify-emotion-decay.ts +0 -148
  141. package/scripts/viz-dashboard.html +0 -423
  142. package/scripts/viz-export.ts +0 -703
  143. package/src/__tests__/access-tracker-novelty.test.ts +0 -228
  144. package/src/__tests__/activity-counter.test.ts +0 -96
  145. package/src/__tests__/admission-control.test.ts +0 -206
  146. package/src/__tests__/anchor-generator.test.ts +0 -67
  147. package/src/__tests__/auto-consolidation.test.ts +0 -168
  148. package/src/__tests__/auto-recall.test.ts +0 -131
  149. package/src/__tests__/babel-integration.test.ts +0 -139
  150. package/src/__tests__/batch-dedup.test.ts +0 -65
  151. package/src/__tests__/capture-engine.test.ts +0 -1144
  152. package/src/__tests__/capture-heuristic.test.ts +0 -171
  153. package/src/__tests__/cascade-forget.test.ts +0 -210
  154. package/src/__tests__/category-min-scores.test.ts +0 -33
  155. package/src/__tests__/cc8-cc1-essential-context.test.ts +0 -298
  156. package/src/__tests__/circuit-breaker.test.ts +0 -99
  157. package/src/__tests__/cluster-consolidation.test.ts +0 -712
  158. package/src/__tests__/confidence-metadata.test.ts +0 -125
  159. package/src/__tests__/confidence-tracker.test.ts +0 -249
  160. package/src/__tests__/conflict-advisor.test.ts +0 -206
  161. package/src/__tests__/conflict-escalation.test.ts +0 -149
  162. package/src/__tests__/conflict-lifecycle.test.ts +0 -83
  163. package/src/__tests__/conflict-output.test.ts +0 -85
  164. package/src/__tests__/conflict-store.test.ts +0 -374
  165. package/src/__tests__/consolidation-engine.test.ts +0 -181
  166. package/src/__tests__/constructive-retrieval.test.ts +0 -509
  167. package/src/__tests__/context-collapse-renderer.test.ts +0 -214
  168. package/src/__tests__/context-composer-pins.test.ts +0 -64
  169. package/src/__tests__/context-composer-task-selection.test.ts +0 -24
  170. package/src/__tests__/context-reconstructor.test.ts +0 -65
  171. package/src/__tests__/context-renderer.test.ts +0 -91
  172. package/src/__tests__/continuity-eval.test.ts +0 -275
  173. package/src/__tests__/continuity-schema.test.ts +0 -97
  174. package/src/__tests__/conversation-importer.test.ts +0 -692
  175. package/src/__tests__/core-recall-eval.test.ts +0 -235
  176. package/src/__tests__/core-summary.test.ts +0 -109
  177. package/src/__tests__/data-checkup.test.ts +0 -225
  178. package/src/__tests__/decay-exemption.test.ts +0 -84
  179. package/src/__tests__/dedup-actions.test.ts +0 -58
  180. package/src/__tests__/dedup-guard.test.ts +0 -39
  181. package/src/__tests__/deterministic-id.test.ts +0 -39
  182. package/src/__tests__/distill-lock.test.ts +0 -176
  183. package/src/__tests__/doctor.test.ts +0 -303
  184. package/src/__tests__/dream-pipeline.test.ts +0 -232
  185. package/src/__tests__/drill-down.test.ts +0 -108
  186. package/src/__tests__/embedder.test.ts +0 -122
  187. package/src/__tests__/emotion-decay.test.ts +0 -46
  188. package/src/__tests__/emotion-detector.test.ts +0 -56
  189. package/src/__tests__/emotion-phase1-integration.test.ts +0 -153
  190. package/src/__tests__/emotion-retrieval.test.ts +0 -56
  191. package/src/__tests__/entity-resolver.test.ts +0 -58
  192. package/src/__tests__/ethics-layer.test.ts +0 -476
  193. package/src/__tests__/event-segmenter.test.ts +0 -153
  194. package/src/__tests__/feature-flags-integration.test.ts +0 -54
  195. package/src/__tests__/frequency-tracker.test.ts +0 -113
  196. package/src/__tests__/graph-export.test.ts +0 -489
  197. package/src/__tests__/hotness-blend.test.ts +0 -76
  198. package/src/__tests__/ingest.test.ts +0 -360
  199. package/src/__tests__/interference-detector.test.ts +0 -203
  200. package/src/__tests__/kg-extractor.test.ts +0 -184
  201. package/src/__tests__/kg-store.test.ts +0 -198
  202. package/src/__tests__/llm-consolidation.test.ts +0 -226
  203. package/src/__tests__/mcp-efficiency.test.ts +0 -288
  204. package/src/__tests__/memory-boundaries.test.ts +0 -237
  205. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -243
  206. package/src/__tests__/memory-evolution.test.ts +0 -271
  207. package/src/__tests__/memory-health-rebalance.test.ts +0 -144
  208. package/src/__tests__/memory-lint.test.ts +0 -271
  209. package/src/__tests__/memory-output.test.ts +0 -215
  210. package/src/__tests__/multi-vector.test.ts +0 -242
  211. package/src/__tests__/narrative-architecture.test.ts +0 -456
  212. package/src/__tests__/pending-review.test.ts +0 -77
  213. package/src/__tests__/post-retrieval-filter.test.ts +0 -178
  214. package/src/__tests__/ppr-traversal.test.ts +0 -256
  215. package/src/__tests__/prediction-engine.test.ts +0 -349
  216. package/src/__tests__/predictive-prospective-memory.test.ts +0 -416
  217. package/src/__tests__/preference-guard-smoke.test.ts +0 -211
  218. package/src/__tests__/preference-matcher.test.ts +0 -230
  219. package/src/__tests__/preference-slots.test.ts +0 -205
  220. package/src/__tests__/prospective-memory.test.ts +0 -283
  221. package/src/__tests__/query-entity-detector.test.ts +0 -171
  222. package/src/__tests__/query-expander-alias.test.ts +0 -89
  223. package/src/__tests__/query-expander.test.ts +0 -34
  224. package/src/__tests__/recall-governor.test.ts +0 -227
  225. package/src/__tests__/recall-mode.test.ts +0 -64
  226. package/src/__tests__/result-synthesizer.test.ts +0 -198
  227. package/src/__tests__/resume-context.test.ts +0 -4898
  228. package/src/__tests__/retrieval-trace.test.ts +0 -213
  229. package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
  230. package/src/__tests__/retroactive-boost.test.ts +0 -223
  231. package/src/__tests__/salience-scoring.test.ts +0 -106
  232. package/src/__tests__/scope-inventory.test.ts +0 -200
  233. package/src/__tests__/scope-policy.test.ts +0 -69
  234. package/src/__tests__/session-checkpoint.test.ts +0 -259
  235. package/src/__tests__/session-distiller.test.ts +0 -611
  236. package/src/__tests__/session-gc.test.ts +0 -215
  237. package/src/__tests__/skill-engine.test.ts +0 -546
  238. package/src/__tests__/skill-promotion.test.ts +0 -349
  239. package/src/__tests__/store-scope-audit.test.ts +0 -81
  240. package/src/__tests__/supersede-chain.test.ts +0 -154
  241. package/src/__tests__/temporal-parser.test.ts +0 -241
  242. package/src/__tests__/temporal-validity.test.ts +0 -160
  243. package/src/__tests__/term-registry.test.ts +0 -121
  244. package/src/__tests__/tool-output-compressor.test.ts +0 -133
  245. package/src/__tests__/topic-tag.test.ts +0 -113
  246. package/src/__tests__/trusted-memory.test.ts +0 -828
  247. package/src/__tests__/version-manager.test.ts +0 -213
  248. package/src/__tests__/workflow-observation.test.ts +0 -400
  249. package/src/__tests__/write-verifier.test.ts +0 -122
@@ -1,782 +0,0 @@
1
- # RecallNest Ingest Pipeline Upgrade Implementation Plan
2
-
3
- > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
-
5
- **Goal:** Convert 23K raw conversation fragments into structured knowledge, then upgrade the ingest pipeline so new ingests always extract before storing.
6
-
7
- **Architecture:** Route B (bulk distillation script) runs first to clean existing data, then Route A (pipeline default change) prevents future garbage. Both share `smartExtractBatch()` as the extraction engine with qwen-turbo LLM.
8
-
9
- **Tech Stack:** Bun + TypeScript, LanceDB, Jina v5 embeddings, qwen-turbo (via OpenAI-compatible API)
10
-
11
- **Spec:** `docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md`
12
-
13
- ---
14
-
15
- ## File Structure
16
-
17
- | File | Action | Responsibility |
18
- |------|--------|----------------|
19
- | `scripts/distill-facts.ts` | **Create** | Bulk distillation: dry-run + batch + checkpoint |
20
- | `src/memory-boundaries.ts` | **Modify** | Add `"distillation"` authority |
21
- | `src/retriever.ts` | **Modify** | Filter `archived` records from search |
22
- | `src/ingest.ts` | **Modify** | Skip raw slices when LLM unavailable |
23
- | `src/cli.ts` | **Modify** | Add `--no-llm` flag |
24
-
25
- ---
26
-
27
- ## Task 1: Add `"distillation"` Authority to Memory Boundaries
28
-
29
- **Files:**
30
- - Modify: `src/memory-boundaries.ts:11-17` (MEMORY_AUTHORITIES array)
31
- - Modify: `src/memory-boundaries.ts:84-94` (add builder function)
32
-
33
- - [ ] **Step 1: Add authority constant**
34
-
35
- In `src/memory-boundaries.ts`, add `"distillation"` to MEMORY_AUTHORITIES:
36
-
37
- ```typescript
38
- export const MEMORY_AUTHORITIES = [
39
- "manual-document",
40
- "structured-memory",
41
- "document-ingest",
42
- "transcript-ingest",
43
- "session-checkpoint",
44
- "distillation",
45
- ] as const;
46
- ```
47
-
48
- - [ ] **Step 2: Add boundary builder function**
49
-
50
- After `buildStructuredMemoryBoundary()` (~line 94), add:
51
-
52
- ```typescript
53
- export function buildDistillationBoundary(
54
- category: DurableMemoryCategory,
55
- ): MemoryBoundaryMetadata {
56
- return {
57
- layer: "durable",
58
- authority: "distillation",
59
- conflictPolicy: getConflictPolicyForCategory(category),
60
- originalCategory: category,
61
- };
62
- }
63
- ```
64
-
65
- - [ ] **Step 3: Verify TypeScript compiles**
66
-
67
- Run: `cd ~/recallnest && bun build src/memory-boundaries.ts --no-bundle 2>&1 | head -5`
68
- Expected: No type errors
69
-
70
- - [ ] **Step 4: Commit**
71
-
72
- ```bash
73
- cd ~/recallnest && git add src/memory-boundaries.ts && git commit -m "feat: add distillation authority to memory boundaries"
74
- ```
75
-
76
- ---
77
-
78
- ## Task 2: Add Archived Filter to Retriever
79
-
80
- **Files:**
81
- - Modify: `src/retriever.ts:70-79` (RetrievalContext)
82
- - Modify: `src/retriever.ts:437-461` (runVectorSearch)
83
- - Modify: `src/retriever.ts:463-480` (runBM25Search)
84
-
85
- - [ ] **Step 1: Add `includeArchived` to RetrievalContext**
86
-
87
- In `src/retriever.ts`, update RetrievalContext interface:
88
-
89
- ```typescript
90
- export interface RetrievalContext {
91
- query: string;
92
- limit: number;
93
- scopeFilter?: string[];
94
- category?: string;
95
- source?: "manual" | "auto-recall" | "cli";
96
- includeArchived?: boolean; // default false — exclude archived records
97
- }
98
- ```
99
-
100
- - [ ] **Step 2: Add `includeArchived` parameter to private search methods**
101
-
102
- `runVectorSearch` and `runBM25Search` receive individual parameters, not the full context. Add `includeArchived?: boolean` to both signatures:
103
-
104
- In `runVectorSearch()` (line ~437):
105
- ```typescript
106
- private async runVectorSearch(
107
- queryVector: number[],
108
- limit: number,
109
- scopeFilter?: string[],
110
- category?: string,
111
- includeArchived?: boolean,
112
- ): Promise<Array<MemorySearchResult & { rank: number }>>
113
- ```
114
-
115
- In `runBM25Search()` (line ~463):
116
- ```typescript
117
- private async runBM25Search(
118
- query: string,
119
- limit: number,
120
- scopeFilter?: string[],
121
- category?: string,
122
- includeArchived?: boolean,
123
- ): Promise<Array<MemorySearchResult & { rank: number }>>
124
- ```
125
-
126
- - [ ] **Step 3: Forward `includeArchived` from retrieve() to private methods**
127
-
128
- In `retrieve()` (line ~309), where it calls `runVectorSearch` and `runBM25Search`, pass `context.includeArchived`:
129
-
130
- ```typescript
131
- // In vectorOnlyRetrieval or hybridRetrieval call sites:
132
- const vectorResults = await this.runVectorSearch(queryVector, limit, scopeFilter, category, context.includeArchived);
133
- const bm25Results = await this.runBM25Search(query, limit, scopeFilter, category, context.includeArchived);
134
- ```
135
-
136
- - [ ] **Step 4: Filter archived records using existing `parseMetadata`**
137
-
138
- In `runVectorSearch()`, after the existing category filter (around line 452-454), add using the existing `parseMetadata` function (already at line ~125 of retriever.ts):
139
-
140
- ```typescript
141
- // Filter archived records (default: exclude)
142
- const afterArchive = includeArchived
143
- ? filtered
144
- : filtered.filter(r => {
145
- const meta = parseMetadata(r.entry.metadata);
146
- return meta.archived !== true;
147
- });
148
- ```
149
-
150
- Use `afterArchive` in place of `filtered` downstream. Same pattern in `runBM25Search()` after category filter (around line 472-474).
151
-
152
- - [ ] **Step 5: Verify TypeScript compiles**
153
-
154
- Run: `cd ~/recallnest && bun build src/retriever.ts --no-bundle 2>&1 | head -5`
155
-
156
- - [ ] **Step 6: Commit**
157
-
158
- ```bash
159
- cd ~/recallnest && git add src/retriever.ts && git commit -m "feat: filter archived records from search results by default"
160
- ```
161
-
162
- ---
163
-
164
- ## Task 3: Create Bulk Distillation Script
165
-
166
- **Files:**
167
- - Create: `scripts/distill-facts.ts`
168
-
169
- This is the largest task. The script:
170
- 1. Reads all `fact` records from LanceDB
171
- 2. Groups by scope (session)
172
- 3. Feeds batches through `smartExtractBatch()`
173
- 4. Stores new durable records, archives originals
174
- 5. Tracks progress for crash recovery
175
-
176
- - [ ] **Step 1: Create the script with imports and config**
177
-
178
- Create `scripts/distill-facts.ts`:
179
-
180
- ```typescript
181
- #!/usr/bin/env bun
182
- /**
183
- * Bulk distillation: Convert raw "fact" conversation fragments into
184
- * structured knowledge using smartExtractBatch().
185
- *
186
- * Usage:
187
- * bun scripts/distill-facts.ts --dry-run # Process 1 session, print results
188
- * bun scripts/distill-facts.ts # Batch all sessions with checkpoint
189
- * bun scripts/distill-facts.ts --scope cc:09541b9c # Process specific session only
190
- */
191
-
192
- import path from "path";
193
- import fs from "fs";
194
- import { MemoryStore, type MemoryEntry } from "../src/store.js";
195
- import { loadConfig, createComponents } from "../src/runtime-config.js";
196
- import { dedupCheck } from "../src/ingest.js";
197
- import { buildDistillationBoundary } from "../src/memory-boundaries.js";
198
- import type { SmartExtraction } from "../src/llm-client.js";
199
-
200
- // ─── Config ─────────────────────────────────────────────────────────────────
201
-
202
- const BATCH_SIZE = 20;
203
- const PROGRESS_FILE = path.join(import.meta.dir, "..", "data", "distill-progress.json");
204
-
205
- interface DistillProgress {
206
- startedAt: string;
207
- lastUpdated: string;
208
- completedScopes: string[];
209
- stats: {
210
- totalFacts: number;
211
- processedFacts: number;
212
- newRecords: number;
213
- dedupedSkips: number;
214
- errors: number;
215
- };
216
- }
217
-
218
- function loadProgress(): DistillProgress | null {
219
- try {
220
- return JSON.parse(fs.readFileSync(PROGRESS_FILE, "utf-8"));
221
- } catch {
222
- return null;
223
- }
224
- }
225
-
226
- function saveProgress(progress: DistillProgress): void {
227
- fs.writeFileSync(PROGRESS_FILE, JSON.stringify(progress, null, 2));
228
- }
229
- ```
230
-
231
- - [ ] **Step 2: Add the core distillation logic**
232
-
233
- Append to `scripts/distill-facts.ts`:
234
-
235
- ```typescript
236
- // ─── Core Logic ─────────────────────────────────────────────────────────────
237
-
238
- async function distillBatch(
239
- texts: string[],
240
- originals: MemoryEntry[],
241
- store: MemoryStore,
242
- embedder: Awaited<ReturnType<typeof createComponents>>["embedder"],
243
- llm: Awaited<ReturnType<typeof createComponents>>["llm"],
244
- dryRun = false,
245
- ): Promise<{ newCount: number; dedupCount: number; errors: number }> {
246
- let newCount = 0;
247
- let dedupCount = 0;
248
- let errors = 0;
249
-
250
- // Step 1: Extract knowledge via LLM
251
- let extractions: SmartExtraction[];
252
- try {
253
- if (!llm) throw new Error("LLM not available");
254
- const llmResults = await llm.smartExtractBatch(texts);
255
- extractions = llmResults.map((r, i) => r ?? null).filter(Boolean) as SmartExtraction[];
256
- if (extractions.length === 0) {
257
- // All failed - skip this batch
258
- return { newCount: 0, dedupCount: 0, errors: texts.length };
259
- }
260
- } catch (e) {
261
- console.error(` ❌ LLM extraction failed: ${e}`);
262
- return { newCount: 0, dedupCount: 0, errors: texts.length };
263
- }
264
-
265
- // Step 2: Generate embeddings for extracted L0 summaries
266
- const extractedTexts = extractions.map(e => e.l0 || e.l1 || "");
267
- const validIndices = extractedTexts.map((t, i) => t.length > 10 ? i : -1).filter(i => i >= 0);
268
-
269
- if (validIndices.length === 0) {
270
- return { newCount: 0, dedupCount: texts.length, errors: 0 };
271
- }
272
-
273
- const textsToEmbed = validIndices.map(i => extractions[i].l1 || extractions[i].l0);
274
- let vectors: number[][];
275
- try {
276
- vectors = await embedder.embedBatchPassage(textsToEmbed);
277
- } catch (e) {
278
- console.error(` ❌ Embedding failed: ${e}`);
279
- return { newCount: 0, dedupCount: 0, errors: validIndices.length };
280
- }
281
-
282
- // Step 3: Dedup + store each extracted record
283
- // Track mapping: original index → new record ID (for correct archive linking)
284
- const idMapping = new Map<number, string>(); // validIndices index → new entry id
285
- for (let vi = 0; vi < validIndices.length; vi++) {
286
- const idx = validIndices[vi];
287
- const extraction = extractions[idx];
288
- const vector = vectors[vi];
289
- const original = originals[idx];
290
-
291
- // Dedup against existing non-fact records
292
- const dedup = await dedupCheck(store, vector, extraction.l0, llm);
293
- if (dedup.action === "skip") {
294
- dedupCount++;
295
- continue;
296
- }
297
-
298
- // Build new record
299
- const boundary = buildDistillationBoundary(extraction.category as any);
300
- const metadata = JSON.stringify({
301
- source: "distillation",
302
- distilledFrom: original.id,
303
- originalScope: original.scope,
304
- l0: extraction.l0,
305
- l1: extraction.l1,
306
- tier: extraction.importance >= 0.8 ? "working" : "peripheral",
307
- boundary,
308
- });
309
-
310
- if (dryRun) {
311
- // Dry-run: print extraction result, don't write
312
- console.log(`\n 📝 [${extraction.category}] importance=${extraction.importance}`);
313
- console.log(` 原文: ${original.text.slice(0, 80)}...`);
314
- console.log(` L0: ${extraction.l0}`);
315
- console.log(` L1: ${(extraction.l1 || "").slice(0, 120)}`);
316
- newCount++;
317
- continue;
318
- }
319
-
320
- try {
321
- const entry = await store.store({
322
- text: extraction.l1 || extraction.l0,
323
- vector,
324
- category: extraction.category as any,
325
- scope: original.scope.replace(/^cc:/, "distill:"),
326
- importance: extraction.importance,
327
- metadata,
328
- });
329
- idMapping.set(idx, entry.id);
330
- newCount++;
331
- } catch (e) {
332
- console.error(` ❌ Store failed: ${e}`);
333
- errors++;
334
- }
335
- }
336
-
337
- // Step 4: Archive originals — each links to its own distilled record (skip in dry-run)
338
- if (dryRun) return { newCount, dedupCount, errors };
339
- for (let vi = 0; vi < validIndices.length; vi++) {
340
- const idx = validIndices[vi];
341
- const original = originals[idx];
342
- const distilledId = idMapping.get(idx);
343
- if (!distilledId) continue; // Was deduped or failed — don't archive
344
- try {
345
- const existingMeta = original.metadata ? JSON.parse(original.metadata) : {};
346
- const updatedMeta = JSON.stringify({
347
- ...existingMeta,
348
- archived: true,
349
- archivedAt: new Date().toISOString(),
350
- distilledTo: [distilledId],
351
- originalCategory: "fact",
352
- });
353
- await store.update(original.id, { metadata: updatedMeta });
354
- } catch (e) {
355
- console.error(` ⚠️ Archive mark failed for ${original.id}: ${e}`);
356
- }
357
- }
358
-
359
- return { newCount, dedupCount, errors };
360
- }
361
- ```
362
-
363
- - [ ] **Step 3: Add the main function**
364
-
365
- Append to `scripts/distill-facts.ts`:
366
-
367
- ```typescript
368
- // ─── Main ───────────────────────────────────────────────────────────────────
369
-
370
- async function main() {
371
- const args = process.argv.slice(2);
372
- const dryRun = args.includes("--dry-run");
373
- const scopeArg = args.find(a => a.startsWith("--scope="))?.split("=")[1]
374
- || (args.includes("--scope") ? args[args.indexOf("--scope") + 1] : undefined);
375
-
376
- const config = loadConfig();
377
- const { store, embedder, llm } = createComponents(config);
378
-
379
- if (!llm) {
380
- console.error("❌ LLM 未配置,无法进行知识提炼。检查 config.json 的 llm 配置。");
381
- process.exit(1);
382
- }
383
-
384
- // Test LLM connectivity
385
- const llmTest = await llm.test();
386
- if (!llmTest.success) {
387
- console.error(`❌ LLM 连接失败: ${llmTest.error}`);
388
- process.exit(1);
389
- }
390
- console.log(`✅ LLM: ${config.llm?.model}`);
391
-
392
- // Fetch all fact records
393
- console.log("\n⏳ 正在读取 fact 记录...");
394
- const allFacts: MemoryEntry[] = await store.list(undefined, "fact", 50000, 0);
395
- console.log(`✅ 共 ${allFacts.length} 条 fact 记录\n`);
396
-
397
- // Filter out already-archived facts
398
- const unarchived = allFacts.filter(f => {
399
- if (!f.metadata) return true;
400
- try { return !JSON.parse(f.metadata).archived; } catch { return true; }
401
- });
402
- console.log(`📋 未归档: ${unarchived.length} 条 (已归档: ${allFacts.length - unarchived.length} 条)\n`);
403
-
404
- // Group by scope
405
- const byScope = new Map<string, MemoryEntry[]>();
406
- for (const f of unarchived) {
407
- const arr = byScope.get(f.scope) || [];
408
- arr.push(f);
409
- byScope.set(f.scope, arr);
410
- }
411
- const scopes = [...byScope.keys()].sort((a, b) => (byScope.get(b)!.length - byScope.get(a)!.length));
412
- console.log(`📦 ${scopes.length} 个 scope\n`);
413
-
414
- // Load or create progress
415
- let progress = loadProgress();
416
- if (!progress) {
417
- progress = {
418
- startedAt: new Date().toISOString(),
419
- lastUpdated: new Date().toISOString(),
420
- completedScopes: [],
421
- stats: { totalFacts: unarchived.length, processedFacts: 0, newRecords: 0, dedupedSkips: 0, errors: 0 },
422
- };
423
- }
424
-
425
- // Filter scopes
426
- let targetScopes = scopes;
427
- if (scopeArg) {
428
- targetScopes = scopes.filter(s => s === scopeArg);
429
- if (targetScopes.length === 0) {
430
- console.error(`❌ scope "${scopeArg}" 不存在`);
431
- process.exit(1);
432
- }
433
- }
434
- if (dryRun) {
435
- targetScopes = [targetScopes[0]]; // Only first scope
436
- console.log(`🔍 DRY RUN: 只处理 scope "${targetScopes[0]}" (${byScope.get(targetScopes[0])!.length} 条)\n`);
437
- }
438
-
439
- // Process scopes
440
- for (const scope of targetScopes) {
441
- if (progress.completedScopes.includes(scope)) {
442
- console.log(`⏭️ ${scope}: 已处理,跳过`);
443
- continue;
444
- }
445
-
446
- const facts = byScope.get(scope)!;
447
- console.log(`\n🔄 ${scope}: ${facts.length} 条 fact`);
448
-
449
- // Process in batches
450
- let scopeNew = 0, scopeDedup = 0, scopeErrors = 0;
451
- for (let i = 0; i < facts.length; i += BATCH_SIZE) {
452
- const batch = facts.slice(i, i + BATCH_SIZE);
453
- const texts = batch.map(f => f.text);
454
-
455
- process.stdout.write(` batch ${Math.floor(i / BATCH_SIZE) + 1}/${Math.ceil(facts.length / BATCH_SIZE)}...`);
456
- const result = await distillBatch(texts, batch, store, embedder, llm, dryRun);
457
-
458
- scopeNew += result.newCount;
459
- scopeDedup += result.dedupCount;
460
- scopeErrors += result.errors;
461
- console.log(` +${result.newCount} new, ${result.dedupCount} dedup, ${result.errors} err`);
462
- }
463
-
464
- console.log(` 📊 ${scope} 完成: +${scopeNew} 新记忆, ${scopeDedup} 去重, ${scopeErrors} 错误`);
465
-
466
- // Update progress
467
- progress.completedScopes.push(scope);
468
- progress.stats.processedFacts += facts.length;
469
- progress.stats.newRecords += scopeNew;
470
- progress.stats.dedupedSkips += scopeDedup;
471
- progress.stats.errors += scopeErrors;
472
- progress.lastUpdated = new Date().toISOString();
473
-
474
- if (!dryRun) {
475
- saveProgress(progress);
476
- }
477
- }
478
-
479
- // Final summary
480
- console.log("\n" + "=".repeat(60));
481
- console.log("📊 提炼完成汇总:");
482
- console.log(` 总 fact 记录: ${progress.stats.totalFacts}`);
483
- console.log(` 已处理: ${progress.stats.processedFacts}`);
484
- console.log(` 新知识记忆: ${progress.stats.newRecords}`);
485
- console.log(` 去重跳过: ${progress.stats.dedupedSkips}`);
486
- console.log(` 错误: ${progress.stats.errors}`);
487
- console.log(` 压缩比: ${(progress.stats.processedFacts / Math.max(progress.stats.newRecords, 1)).toFixed(1)}:1`);
488
- if (dryRun) {
489
- console.log("\n⚠️ DRY RUN 模式,未保存进度。确认质量后去掉 --dry-run 再跑。");
490
- }
491
- }
492
-
493
- main().catch(console.error);
494
- ```
495
-
496
- - [ ] **Step 4: Verify script compiles**
497
-
498
- Run: `cd ~/recallnest && bun build scripts/distill-facts.ts --no-bundle 2>&1 | head -10`
499
-
500
- - [ ] **Step 5: Commit**
501
-
502
- ```bash
503
- cd ~/recallnest && git add scripts/distill-facts.ts && git commit -m "feat: add bulk distillation script for converting fact fragments to knowledge"
504
- ```
505
-
506
- ---
507
-
508
- ## Task 4: Dry-Run Test
509
-
510
- - [ ] **Step 1: Run dry-run on first session**
511
-
512
- ```bash
513
- cd ~/recallnest && bun scripts/distill-facts.ts --dry-run 2>&1 | tee /tmp/distill-dry-run.log
514
- ```
515
-
516
- Expected: Processes 1 session (~30-50 facts), prints extraction results, does NOT save progress.
517
-
518
- - [ ] **Step 2: Review output quality**
519
-
520
- Check the log:
521
- - Are extractions meaningful? (not just repeating the original text)
522
- - Are categories correct? (profile/preferences/entities/events/cases/patterns)
523
- - Is importance scoring reasonable?
524
- - Is dedup catching true duplicates?
525
-
526
- If quality is poor → adjust `smartExtractBatch` prompts in `src/llm-client.ts` before proceeding.
527
-
528
- - [ ] **Step 3: Get user sign-off**
529
-
530
- Show the dry-run results to user. Only proceed to batch if user approves quality.
531
-
532
- ---
533
-
534
- ## Task 5: Batch Distillation Run
535
-
536
- **Prerequisite:** Task 4 dry-run approved by user.
537
-
538
- - [ ] **Step 1: Run full batch**
539
-
540
- ```bash
541
- cd ~/recallnest && bun scripts/distill-facts.ts 2>&1 | tee /tmp/distill-batch.log
542
- ```
543
-
544
- This will:
545
- - Process all ~700 sessions
546
- - Write checkpoint after each session (crash-safe)
547
- - Skip already-completed sessions on resume
548
-
549
- Estimated time: ~30-60 minutes (depends on qwen-turbo latency)
550
-
551
- - [ ] **Step 2: Monitor progress**
552
-
553
- If interrupted, resume with same command — it reads `data/distill-progress.json` and skips completed scopes.
554
-
555
- - [ ] **Step 3: Verify results**
556
-
557
- ```bash
558
- cd ~/recallnest && bun src/cli.ts stats
559
- ```
560
-
561
- Expected:
562
- - `fact` count should still be ~23K (archived but not deleted)
563
- - New categories (entities, cases, patterns, etc.) should increase by ~3-5K
564
- - Total records ~33-35K
565
-
566
- - [ ] **Step 4: Run health check**
567
-
568
- ```bash
569
- cd ~/recallnest && bun scripts/health-check.ts
570
- ```
571
-
572
- Compare dead memory rate before/after. Should drop significantly.
573
-
574
- - [ ] **Step 5: Commit progress file**
575
-
576
- ```bash
577
- cd ~/recallnest && git add data/distill-progress.json && git commit -m "data: distillation batch complete — 23K facts processed"
578
- ```
579
-
580
- **Rollback plan** (if results are poor): Un-archive originals and delete distilled records:
581
- ```bash
582
- cd ~/recallnest && bun -e "
583
- import lancedb from '@lancedb/lancedb';
584
- const db = await lancedb.connect('data/lancedb');
585
- const t = await db.openTable('memories');
586
- // Find all distilled records and their source originals
587
- const distilled = await t.query().where(\"metadata LIKE '%distillation%'\").toArray();
588
- console.log('Would delete', distilled.length, 'distilled records');
589
- // To actually rollback: delete distilled, then un-archive originals by removing archived flag
590
- "
591
- ```
592
-
593
- ---
594
-
595
- ## Task 6: Upgrade Ingest Pipeline (Route A)
596
-
597
- **Files:**
598
- - Modify: `src/ingest.ts:337-352` (smartExtractBatch behavior)
599
- - Modify: `src/cli.ts:1281-1287` (add --no-llm flag)
600
-
601
- - [ ] **Step 1: Change ingest behavior when LLM unavailable**
602
-
603
- In `src/ingest.ts`, modify the section where chunks are stored. The key change is: when `llm` is null and `smartExtractBatch` returns fallback results, log a warning and skip storing the raw chunk. Instead, append to a pending queue file.
604
-
605
- Add a helper function after the existing `fallbackExtraction` function (~line 332):
606
-
607
- ```typescript
608
- const PENDING_EXTRACTION_FILE = path.join(
609
- import.meta.dir, "..", "data", "pending-extraction.json"
610
- );
611
-
612
- function queueForLaterExtraction(chunk: { text: string; scope: string; timestamp: string }): void {
613
- let pending: Array<typeof chunk> = [];
614
- try {
615
- pending = JSON.parse(fs.readFileSync(PENDING_EXTRACTION_FILE, "utf-8"));
616
- } catch { /* empty or missing */ }
617
- pending.push(chunk);
618
- fs.writeFileSync(PENDING_EXTRACTION_FILE, JSON.stringify(pending, null, 2));
619
- }
620
- ```
621
-
622
- Add import at top of `ingest.ts`:
623
- ```typescript
624
- import fs from "fs";
625
- import path from "path";
626
- ```
627
-
628
- - [ ] **Step 2: Guard the store path in ingestCCTranscripts**
629
-
630
- In `ingestCCTranscripts()`, around the `smartExtractBatch` call (line ~1138), wrap the store logic:
631
-
632
- ```typescript
633
- const extractions = await smartExtractBatch(dedupedTexts, options.llm);
634
-
635
- // If no LLM, queue raw chunks for later extraction instead of storing garbage
636
- if (!options.llm) {
637
- for (let i = 0; i < dedupedTexts.length; i++) {
638
- queueForLaterExtraction({
639
- text: dedupedTexts[i],
640
- scope: `cc:${sessionId.slice(0, 8)}`,
641
- timestamp: chunks[0]?.timestamp || new Date().toISOString(),
642
- });
643
- }
644
- result.chunksSkipped = (result.chunksSkipped || 0) + dedupedTexts.length;
645
- continue; // Skip to next file
646
- }
647
- ```
648
-
649
- Apply the same guard in `ingestCodexSessions()` (line ~667, same pattern — check `!options.llm` before the store path, queue chunks instead) and `ingestGeminiSessions()` (line ~848).
650
-
651
- - [ ] **Step 3: Add pending queue drain at start of ingest**
652
-
653
- In `src/ingest.ts`, add a function to process the pending queue when LLM is available:
654
-
655
- ```typescript
656
- export async function drainPendingQueue(
657
- store: MemoryStore,
658
- embedder: Embedder,
659
- llm: LLMClient,
660
- ): Promise<{ processed: number; errors: number }> {
661
- let pending: Array<{ text: string; scope: string; timestamp: string }> = [];
662
- try {
663
- pending = JSON.parse(fs.readFileSync(PENDING_EXTRACTION_FILE, "utf-8"));
664
- } catch { return { processed: 0, errors: 0 }; }
665
-
666
- if (pending.length === 0) return { processed: 0, errors: 0 };
667
-
668
- let processed = 0, errors = 0;
669
- for (let i = 0; i < pending.length; i += 20) {
670
- const batch = pending.slice(i, i + 20);
671
- const texts = batch.map(c => c.text);
672
- const extractions = await smartExtractBatch(texts, llm);
673
- const vectors = await embedder.embedBatchPassage(
674
- extractions.map(e => e.l1 || e.l0),
675
- );
676
- for (let j = 0; j < extractions.length; j++) {
677
- try {
678
- await store.store({
679
- text: extractions[j].l1 || extractions[j].l0,
680
- vector: vectors[j],
681
- category: extractions[j].category as any,
682
- scope: batch[j].scope,
683
- importance: extractions[j].importance,
684
- metadata: JSON.stringify({ source: batch[j].scope.split(":")[0], l0: extractions[j].l0 }),
685
- });
686
- processed++;
687
- } catch { errors++; }
688
- }
689
- }
690
-
691
- // Clear the queue
692
- fs.writeFileSync(PENDING_EXTRACTION_FILE, "[]");
693
- return { processed, errors };
694
- }
695
- ```
696
-
697
- In `src/cli.ts`, call `drainPendingQueue` at the start of the ingest action (after LLM test, around line 1324), before processing new files:
698
-
699
- ```typescript
700
- if (llm) {
701
- const { drainPendingQueue } = await import("./ingest.js");
702
- const drained = await drainPendingQueue(store, embedder, llm);
703
- if (drained.processed > 0) {
704
- console.log(` ♻️ Pending queue: ${drained.processed} processed, ${drained.errors} errors`);
705
- }
706
- }
707
- ```
708
-
709
- - [ ] **Step 4: Add `--no-llm` CLI flag (and update step numbering)**
710
-
711
- In `src/cli.ts`, add the flag to the ingest command (around line 1287):
712
-
713
- ```typescript
714
- .option("--no-llm", "禁用 LLM 提取(仅用于调试,会跳过原始对话不存入)")
715
- ```
716
-
717
- And in the action handler (around line 1326):
718
-
719
- ```typescript
720
- const effectiveLlm = options.noLlm ? null : llm;
721
- const ingestOpts = { limit, verbose, noDedup, llm: effectiveLlm };
722
- ```
723
-
724
- Update the LLM status message (around line 1319-1321):
725
-
726
- ```typescript
727
- if (options.noLlm) {
728
- console.log(" ⚠️ LLM: 已通过 --no-llm 禁用,原始对话将跳过不存入");
729
- } else if (!llm) {
730
- console.log(" ⚠️ LLM: 未配置,原始对话将跳过不存入(配置 config.json → llm 以启用)");
731
- }
732
- ```
733
-
734
- - [ ] **Step 5: Verify TypeScript compiles**
735
-
736
- Run: `cd ~/recallnest && bun build src/ingest.ts --no-bundle 2>&1 | head -5 && bun build src/cli.ts --no-bundle 2>&1 | head -5`
737
-
738
- - [ ] **Step 6: Test ingest with LLM**
739
-
740
- ```bash
741
- cd ~/recallnest && bun src/cli.ts ingest --source cc --limit 1 --verbose
742
- ```
743
-
744
- Expected: Should use LLM extraction, store structured records (not raw slices).
745
-
746
- - [ ] **Step 7: Test ingest with --no-llm**
747
-
748
- ```bash
749
- cd ~/recallnest && bun src/cli.ts ingest --source cc --limit 1 --no-llm --verbose
750
- ```
751
-
752
- Expected: Should skip storing, queue to `data/pending-extraction.json`.
753
-
754
- - [ ] **Step 8: Commit**
755
-
756
- ```bash
757
- cd ~/recallnest && git add src/ingest.ts src/cli.ts && git commit -m "feat: ingest pipeline now requires LLM extraction — raw slices queued when LLM unavailable"
758
- ```
759
-
760
- ---
761
-
762
- ## Task 7: Final Verification & Push
763
-
764
- - [ ] **Step 1: Run full stats**
765
-
766
- ```bash
767
- cd ~/recallnest && bun src/cli.ts stats
768
- ```
769
-
770
- - [ ] **Step 2: Test search excludes archived**
771
-
772
- ```bash
773
- cd ~/recallnest && bun src/cli.ts search "OpenClaw 压缩上下文"
774
- ```
775
-
776
- Verify results come from distilled records, not raw `[助手]`/`[用户]` fragments.
777
-
778
- - [ ] **Step 3: Push all changes**
779
-
780
- ```bash
781
- cd ~/recallnest && git push origin main
782
- ```