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
@@ -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
- ```