recallnest 2.2.0 → 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 (232) hide show
  1. package/.env +4 -0
  2. package/config.json +55 -0
  3. package/logs/daily-recall.error.log +0 -0
  4. package/logs/daily-recall.log +30 -0
  5. package/logs/ingest-2026-04-11.log +110 -0
  6. package/logs/ingest-2026-04-12.log +90 -0
  7. package/logs/launchagent-stderr.log +0 -0
  8. package/logs/launchagent-stdout.log +0 -0
  9. package/package.json +1 -1
  10. package/.claude-plugin/CLAUDE.md +0 -29
  11. package/.claude-plugin/marketplace.json +0 -18
  12. package/.dockerignore +0 -11
  13. package/.github/workflows/ci.yml +0 -28
  14. package/.mcp.json +0 -9
  15. package/CHANGELOG.md +0 -51
  16. package/DEMO.md +0 -94
  17. package/Dockerfile +0 -20
  18. package/EVAL.md +0 -89
  19. package/FAILURES.md +0 -75
  20. package/OPERATIONS.md +0 -59
  21. package/ROADMAP.md +0 -245
  22. package/SKILL.md +0 -250
  23. package/assets/dashboard.png +0 -0
  24. package/assets/knowledge-graph.png +0 -0
  25. package/assets/recallnest-workbench.png +0 -0
  26. package/assets/screenshots/ui-full.png +0 -0
  27. package/assets/screenshots/ui-overview.png +0 -0
  28. package/assets/screenshots/ui-search.png +0 -0
  29. package/assets/ui/app.js +0 -1031
  30. package/assets/ui/index.html +0 -224
  31. package/assets/ui/styles.css +0 -781
  32. package/assets/wechat-qr.jpg +0 -0
  33. package/bun.lock +0 -337
  34. package/daily-reflections/2026-03-05.md +0 -15
  35. package/daily-reflections/2026-03-06.md +0 -15
  36. package/daily-reflections/2026-03-07.md +0 -7
  37. package/daily-reflections/2026-03-08.md +0 -34
  38. package/daily-reflections/2026-03-09.md +0 -35
  39. package/daily-reflections/2026-03-10.md +0 -36
  40. package/daily-reflections/2026-03-11.md +0 -35
  41. package/daily-reflections/2026-03-12.md +0 -36
  42. package/daily-reflections/2026-03-13.md +0 -35
  43. package/daily-reflections/2026-03-14.md +0 -35
  44. package/daily-reflections/2026-03-15.md +0 -35
  45. package/daily-reflections/2026-03-16.md +0 -41
  46. package/daily-reflections/2026-03-17.md +0 -10
  47. package/daily-reflections/2026-03-18.md +0 -10
  48. package/daily-reflections/2026-03-19.md +0 -10
  49. package/daily-reflections/2026-03-20.md +0 -10
  50. package/daily-reflections/2026-03-21.md +0 -10
  51. package/daily-reflections/2026-03-22.md +0 -10
  52. package/daily-reflections/2026-03-23.md +0 -10
  53. package/daily-reflections/2026-03-24.md +0 -10
  54. package/daily-reflections/2026-03-25.md +0 -32
  55. package/daily-reflections/2026-03-26.md +0 -37
  56. package/daily-reflections/2026-03-27.md +0 -35
  57. package/daily-reflections/2026-03-28.md +0 -35
  58. package/daily-reflections/2026-03-29.md +0 -35
  59. package/daily-reflections/2026-03-30.md +0 -36
  60. package/daily-reflections/2026-03-31.md +0 -35
  61. package/daily-reflections/2026-04-01.md +0 -35
  62. package/daily-reflections/2026-04-02.md +0 -35
  63. package/daily-reflections/2026-04-03.md +0 -37
  64. package/daily-reflections/2026-04-04.md +0 -37
  65. package/daily-reflections/2026-04-05.md +0 -35
  66. package/daily-reflections/2026-04-06.md +0 -36
  67. package/daily-reflections/2026-04-07.md +0 -35
  68. package/daily-reflections/2026-04-08.md +0 -35
  69. package/daily-reflections/2026-04-12.md +0 -35
  70. package/data/alias-map.example.json +0 -22
  71. package/docs/api-reference.md +0 -1291
  72. package/docs/architecture.md +0 -168
  73. package/docs/memory-boundary-contract.md +0 -226
  74. package/docs/memory-categories.md +0 -119
  75. package/docs/pipeline-audit-ultramemory.md +0 -177
  76. package/docs/plans/2026-03-08-query-expansion.md +0 -270
  77. package/docs/plans/2026-03-16-continuity-layer.md +0 -196
  78. package/docs/plans/2026-04-01-recall-boost.md +0 -100
  79. package/docs/plans/mcp-tiering-plan.md +0 -126
  80. package/docs/research/recall-quality-audit-2026-03-22.md +0 -153
  81. package/docs/self-evolution.md +0 -198
  82. package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +0 -782
  83. package/docs/superpowers/plans/2026-04-11-constructive-retrieval-emotion.md +0 -1195
  84. package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +0 -77
  85. package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +0 -95
  86. package/docs/superpowers/specs/2026-04-11-constructive-retrieval-emotion-design.md +0 -398
  87. package/eval/continuity/results-2026-03-16.md +0 -117
  88. package/eval/continuity/results-2026-03-19.md +0 -453
  89. package/eval/continuity/runbook.md +0 -110
  90. package/eval/continuity/seed-cases.md +0 -54
  91. package/eval/reports/2026-03-06-abstract-query-baseline.md +0 -96
  92. package/eval/reports/2026-03-06-aws-ssh-baseline.md +0 -82
  93. package/eval/reports/2026-03-06-baseline.md +0 -82
  94. package/eval/reports/query-expansion.md +0 -138
  95. package/glama.json +0 -6
  96. package/lm +0 -19
  97. package/plan-philosophy-upgrades.md +0 -363
  98. package/plan-product-upgrades.md +0 -155
  99. package/plan-v1.2-distributable.md +0 -105
  100. package/plan.md +0 -140
  101. package/research-memory-evolution.md +0 -440
  102. package/research-rl-agent-memory.md +0 -330
  103. package/scripts/archive-distilled-junk.ts +0 -116
  104. package/scripts/backfill-anchors.ts +0 -130
  105. package/scripts/backfill-l0l1l2.ts +0 -98
  106. package/scripts/blind-scan.ts +0 -588
  107. package/scripts/cleanup-step1-exact-dups.ts +0 -117
  108. package/scripts/cleanup-step2-template-dups.ts +0 -113
  109. package/scripts/cleanup-step3-stale-content.ts +0 -149
  110. package/scripts/cleanup-step4-orphan-scopes.ts +0 -131
  111. package/scripts/delete-distilled-junk.ts +0 -88
  112. package/scripts/diagnose-vector-search.ts +0 -132
  113. package/scripts/distill-facts.ts +0 -305
  114. package/scripts/health-check.ts +0 -343
  115. package/scripts/incremental-ingest.sh +0 -71
  116. package/scripts/migrate-language.ts +0 -75
  117. package/scripts/mlp-health-check.ts +0 -382
  118. package/scripts/pull-claude-desktop.ts +0 -515
  119. package/scripts/re-ingest-cc.sh +0 -66
  120. package/scripts/rebalance-memory-health.ts +0 -181
  121. package/scripts/run-desktop-ingest.sh +0 -11
  122. package/scripts/start-server.sh +0 -12
  123. package/scripts/verify-emotion-decay.ts +0 -148
  124. package/scripts/viz-dashboard.html +0 -423
  125. package/scripts/viz-export.ts +0 -703
  126. package/src/__tests__/access-tracker-novelty.test.ts +0 -228
  127. package/src/__tests__/activity-counter.test.ts +0 -96
  128. package/src/__tests__/admission-control.test.ts +0 -206
  129. package/src/__tests__/anchor-generator.test.ts +0 -67
  130. package/src/__tests__/auto-consolidation.test.ts +0 -168
  131. package/src/__tests__/auto-recall.test.ts +0 -131
  132. package/src/__tests__/babel-integration.test.ts +0 -139
  133. package/src/__tests__/batch-dedup.test.ts +0 -65
  134. package/src/__tests__/capture-engine.test.ts +0 -1144
  135. package/src/__tests__/capture-heuristic.test.ts +0 -171
  136. package/src/__tests__/cascade-forget.test.ts +0 -210
  137. package/src/__tests__/category-min-scores.test.ts +0 -33
  138. package/src/__tests__/cc8-cc1-essential-context.test.ts +0 -298
  139. package/src/__tests__/circuit-breaker.test.ts +0 -99
  140. package/src/__tests__/cluster-consolidation.test.ts +0 -712
  141. package/src/__tests__/confidence-metadata.test.ts +0 -125
  142. package/src/__tests__/confidence-tracker.test.ts +0 -249
  143. package/src/__tests__/conflict-advisor.test.ts +0 -206
  144. package/src/__tests__/conflict-escalation.test.ts +0 -149
  145. package/src/__tests__/conflict-lifecycle.test.ts +0 -83
  146. package/src/__tests__/conflict-output.test.ts +0 -85
  147. package/src/__tests__/conflict-store.test.ts +0 -374
  148. package/src/__tests__/consolidation-engine.test.ts +0 -181
  149. package/src/__tests__/constructive-retrieval.test.ts +0 -509
  150. package/src/__tests__/context-collapse-renderer.test.ts +0 -214
  151. package/src/__tests__/context-composer-pins.test.ts +0 -64
  152. package/src/__tests__/context-composer-task-selection.test.ts +0 -24
  153. package/src/__tests__/context-reconstructor.test.ts +0 -65
  154. package/src/__tests__/context-renderer.test.ts +0 -91
  155. package/src/__tests__/continuity-eval.test.ts +0 -275
  156. package/src/__tests__/continuity-schema.test.ts +0 -97
  157. package/src/__tests__/conversation-importer.test.ts +0 -692
  158. package/src/__tests__/core-recall-eval.test.ts +0 -235
  159. package/src/__tests__/core-summary.test.ts +0 -109
  160. package/src/__tests__/data-checkup.test.ts +0 -225
  161. package/src/__tests__/decay-exemption.test.ts +0 -84
  162. package/src/__tests__/dedup-actions.test.ts +0 -58
  163. package/src/__tests__/dedup-guard.test.ts +0 -39
  164. package/src/__tests__/deterministic-id.test.ts +0 -39
  165. package/src/__tests__/distill-lock.test.ts +0 -176
  166. package/src/__tests__/doctor.test.ts +0 -303
  167. package/src/__tests__/dream-pipeline.test.ts +0 -232
  168. package/src/__tests__/drill-down.test.ts +0 -108
  169. package/src/__tests__/embedder.test.ts +0 -122
  170. package/src/__tests__/emotion-decay.test.ts +0 -46
  171. package/src/__tests__/emotion-detector.test.ts +0 -56
  172. package/src/__tests__/emotion-phase1-integration.test.ts +0 -153
  173. package/src/__tests__/emotion-retrieval.test.ts +0 -56
  174. package/src/__tests__/entity-resolver.test.ts +0 -58
  175. package/src/__tests__/ethics-layer.test.ts +0 -476
  176. package/src/__tests__/event-segmenter.test.ts +0 -153
  177. package/src/__tests__/feature-flags-integration.test.ts +0 -54
  178. package/src/__tests__/frequency-tracker.test.ts +0 -113
  179. package/src/__tests__/graph-export.test.ts +0 -489
  180. package/src/__tests__/hotness-blend.test.ts +0 -76
  181. package/src/__tests__/ingest.test.ts +0 -360
  182. package/src/__tests__/interference-detector.test.ts +0 -203
  183. package/src/__tests__/kg-extractor.test.ts +0 -184
  184. package/src/__tests__/kg-store.test.ts +0 -198
  185. package/src/__tests__/llm-consolidation.test.ts +0 -226
  186. package/src/__tests__/mcp-efficiency.test.ts +0 -288
  187. package/src/__tests__/memory-boundaries.test.ts +0 -237
  188. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -243
  189. package/src/__tests__/memory-evolution.test.ts +0 -271
  190. package/src/__tests__/memory-health-rebalance.test.ts +0 -144
  191. package/src/__tests__/memory-lint.test.ts +0 -271
  192. package/src/__tests__/memory-output.test.ts +0 -215
  193. package/src/__tests__/multi-vector.test.ts +0 -242
  194. package/src/__tests__/narrative-architecture.test.ts +0 -456
  195. package/src/__tests__/pending-review.test.ts +0 -77
  196. package/src/__tests__/post-retrieval-filter.test.ts +0 -178
  197. package/src/__tests__/ppr-traversal.test.ts +0 -256
  198. package/src/__tests__/prediction-engine.test.ts +0 -349
  199. package/src/__tests__/predictive-prospective-memory.test.ts +0 -416
  200. package/src/__tests__/preference-guard-smoke.test.ts +0 -211
  201. package/src/__tests__/preference-matcher.test.ts +0 -230
  202. package/src/__tests__/preference-slots.test.ts +0 -205
  203. package/src/__tests__/prospective-memory.test.ts +0 -283
  204. package/src/__tests__/query-entity-detector.test.ts +0 -171
  205. package/src/__tests__/query-expander-alias.test.ts +0 -89
  206. package/src/__tests__/query-expander.test.ts +0 -34
  207. package/src/__tests__/recall-governor.test.ts +0 -227
  208. package/src/__tests__/recall-mode.test.ts +0 -64
  209. package/src/__tests__/result-synthesizer.test.ts +0 -198
  210. package/src/__tests__/resume-context.test.ts +0 -4898
  211. package/src/__tests__/retrieval-trace.test.ts +0 -213
  212. package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
  213. package/src/__tests__/retroactive-boost.test.ts +0 -223
  214. package/src/__tests__/salience-scoring.test.ts +0 -106
  215. package/src/__tests__/scope-inventory.test.ts +0 -200
  216. package/src/__tests__/scope-policy.test.ts +0 -69
  217. package/src/__tests__/session-checkpoint.test.ts +0 -259
  218. package/src/__tests__/session-distiller.test.ts +0 -611
  219. package/src/__tests__/session-gc.test.ts +0 -215
  220. package/src/__tests__/skill-engine.test.ts +0 -546
  221. package/src/__tests__/skill-promotion.test.ts +0 -349
  222. package/src/__tests__/store-scope-audit.test.ts +0 -81
  223. package/src/__tests__/supersede-chain.test.ts +0 -154
  224. package/src/__tests__/temporal-parser.test.ts +0 -241
  225. package/src/__tests__/temporal-validity.test.ts +0 -160
  226. package/src/__tests__/term-registry.test.ts +0 -121
  227. package/src/__tests__/tool-output-compressor.test.ts +0 -133
  228. package/src/__tests__/topic-tag.test.ts +0 -113
  229. package/src/__tests__/trusted-memory.test.ts +0 -828
  230. package/src/__tests__/version-manager.test.ts +0 -213
  231. package/src/__tests__/workflow-observation.test.ts +0 -400
  232. 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
- ```