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.
- package/.env +4 -0
- package/CLAUDE.md +1 -1
- package/README.md +30 -5
- package/README_CN.md +30 -5
- package/config.json +55 -0
- package/connectors/examples/email-adapter.ts +73 -0
- package/connectors/examples/logs-adapter.ts +103 -0
- package/connectors/examples/rss-adapter.ts +95 -0
- package/logs/daily-recall.error.log +0 -0
- package/logs/daily-recall.log +60 -0
- package/logs/ingest-2026-04-11.log +110 -0
- package/logs/ingest-2026-04-12.log +90 -0
- package/logs/ingest-2026-04-13.log +87 -0
- package/logs/launchagent-stderr.log +0 -0
- package/logs/launchagent-stdout.log +0 -0
- package/package.json +1 -1
- package/src/cli.ts +30 -0
- package/src/connector-types.ts +57 -0
- package/src/conversation-importer.ts +25 -1
- package/src/data-checkup.ts +31 -1
- package/src/doctor.ts +29 -0
- package/src/ingest.ts +203 -0
- package/src/mcp-server.ts +2 -2
- package/src/obsidian-connector.ts +282 -0
- package/src/session-schema.ts +2 -2
- package/src/source-heartbeat.ts +108 -0
- package/.claude-plugin/CLAUDE.md +0 -29
- package/.claude-plugin/marketplace.json +0 -18
- package/.dockerignore +0 -11
- package/.github/workflows/ci.yml +0 -28
- package/.mcp.json +0 -9
- package/CHANGELOG.md +0 -51
- package/DEMO.md +0 -94
- package/Dockerfile +0 -20
- package/EVAL.md +0 -89
- package/FAILURES.md +0 -75
- package/OPERATIONS.md +0 -59
- package/ROADMAP.md +0 -245
- package/SKILL.md +0 -250
- package/assets/dashboard.png +0 -0
- package/assets/knowledge-graph.png +0 -0
- package/assets/recallnest-workbench.png +0 -0
- package/assets/screenshots/ui-full.png +0 -0
- package/assets/screenshots/ui-overview.png +0 -0
- package/assets/screenshots/ui-search.png +0 -0
- package/assets/ui/app.js +0 -1031
- package/assets/ui/index.html +0 -224
- package/assets/ui/styles.css +0 -781
- package/assets/wechat-qr.jpg +0 -0
- package/bun.lock +0 -337
- package/daily-reflections/2026-03-05.md +0 -15
- package/daily-reflections/2026-03-06.md +0 -15
- package/daily-reflections/2026-03-07.md +0 -7
- package/daily-reflections/2026-03-08.md +0 -34
- package/daily-reflections/2026-03-09.md +0 -35
- package/daily-reflections/2026-03-10.md +0 -36
- package/daily-reflections/2026-03-11.md +0 -35
- package/daily-reflections/2026-03-12.md +0 -36
- package/daily-reflections/2026-03-13.md +0 -35
- package/daily-reflections/2026-03-14.md +0 -35
- package/daily-reflections/2026-03-15.md +0 -35
- package/daily-reflections/2026-03-16.md +0 -41
- package/daily-reflections/2026-03-17.md +0 -10
- package/daily-reflections/2026-03-18.md +0 -10
- package/daily-reflections/2026-03-19.md +0 -10
- package/daily-reflections/2026-03-20.md +0 -10
- package/daily-reflections/2026-03-21.md +0 -10
- package/daily-reflections/2026-03-22.md +0 -10
- package/daily-reflections/2026-03-23.md +0 -10
- package/daily-reflections/2026-03-24.md +0 -10
- package/daily-reflections/2026-03-25.md +0 -32
- package/daily-reflections/2026-03-26.md +0 -37
- package/daily-reflections/2026-03-27.md +0 -35
- package/daily-reflections/2026-03-28.md +0 -35
- package/daily-reflections/2026-03-29.md +0 -35
- package/daily-reflections/2026-03-30.md +0 -36
- package/daily-reflections/2026-03-31.md +0 -35
- package/daily-reflections/2026-04-01.md +0 -35
- package/daily-reflections/2026-04-02.md +0 -35
- package/daily-reflections/2026-04-03.md +0 -37
- package/daily-reflections/2026-04-04.md +0 -37
- package/daily-reflections/2026-04-05.md +0 -35
- package/daily-reflections/2026-04-06.md +0 -36
- package/daily-reflections/2026-04-07.md +0 -35
- package/daily-reflections/2026-04-08.md +0 -35
- package/daily-reflections/2026-04-12.md +0 -35
- package/data/alias-map.example.json +0 -22
- package/docs/api-reference.md +0 -1291
- package/docs/architecture.md +0 -168
- package/docs/memory-boundary-contract.md +0 -226
- package/docs/memory-categories.md +0 -119
- package/docs/pipeline-audit-ultramemory.md +0 -177
- package/docs/plans/2026-03-08-query-expansion.md +0 -270
- package/docs/plans/2026-03-16-continuity-layer.md +0 -196
- package/docs/plans/2026-04-01-recall-boost.md +0 -100
- package/docs/plans/mcp-tiering-plan.md +0 -126
- package/docs/research/recall-quality-audit-2026-03-22.md +0 -153
- package/docs/self-evolution.md +0 -198
- package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +0 -782
- package/docs/superpowers/plans/2026-04-11-constructive-retrieval-emotion.md +0 -1195
- package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +0 -77
- package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +0 -95
- package/docs/superpowers/specs/2026-04-11-constructive-retrieval-emotion-design.md +0 -398
- package/eval/continuity/results-2026-03-16.md +0 -117
- package/eval/continuity/results-2026-03-19.md +0 -453
- package/eval/continuity/runbook.md +0 -110
- package/eval/continuity/seed-cases.md +0 -54
- package/eval/reports/2026-03-06-abstract-query-baseline.md +0 -96
- package/eval/reports/2026-03-06-aws-ssh-baseline.md +0 -82
- package/eval/reports/2026-03-06-baseline.md +0 -82
- package/eval/reports/query-expansion.md +0 -138
- package/glama.json +0 -6
- package/lm +0 -19
- package/plan-philosophy-upgrades.md +0 -363
- package/plan-product-upgrades.md +0 -155
- package/plan-v1.2-distributable.md +0 -105
- package/plan.md +0 -140
- package/research-memory-evolution.md +0 -440
- package/research-rl-agent-memory.md +0 -330
- package/scripts/archive-distilled-junk.ts +0 -116
- package/scripts/backfill-anchors.ts +0 -130
- package/scripts/backfill-l0l1l2.ts +0 -98
- package/scripts/blind-scan.ts +0 -588
- package/scripts/cleanup-step1-exact-dups.ts +0 -117
- package/scripts/cleanup-step2-template-dups.ts +0 -113
- package/scripts/cleanup-step3-stale-content.ts +0 -149
- package/scripts/cleanup-step4-orphan-scopes.ts +0 -131
- package/scripts/delete-distilled-junk.ts +0 -88
- package/scripts/diagnose-vector-search.ts +0 -132
- package/scripts/distill-facts.ts +0 -305
- package/scripts/health-check.ts +0 -343
- package/scripts/incremental-ingest.sh +0 -71
- package/scripts/migrate-language.ts +0 -75
- package/scripts/mlp-health-check.ts +0 -382
- package/scripts/pull-claude-desktop.ts +0 -515
- package/scripts/re-ingest-cc.sh +0 -66
- package/scripts/rebalance-memory-health.ts +0 -181
- package/scripts/run-desktop-ingest.sh +0 -11
- package/scripts/start-server.sh +0 -12
- package/scripts/verify-emotion-decay.ts +0 -148
- package/scripts/viz-dashboard.html +0 -423
- package/scripts/viz-export.ts +0 -703
- package/src/__tests__/access-tracker-novelty.test.ts +0 -228
- package/src/__tests__/activity-counter.test.ts +0 -96
- package/src/__tests__/admission-control.test.ts +0 -206
- package/src/__tests__/anchor-generator.test.ts +0 -67
- package/src/__tests__/auto-consolidation.test.ts +0 -168
- package/src/__tests__/auto-recall.test.ts +0 -131
- package/src/__tests__/babel-integration.test.ts +0 -139
- package/src/__tests__/batch-dedup.test.ts +0 -65
- package/src/__tests__/capture-engine.test.ts +0 -1144
- package/src/__tests__/capture-heuristic.test.ts +0 -171
- package/src/__tests__/cascade-forget.test.ts +0 -210
- package/src/__tests__/category-min-scores.test.ts +0 -33
- package/src/__tests__/cc8-cc1-essential-context.test.ts +0 -298
- package/src/__tests__/circuit-breaker.test.ts +0 -99
- package/src/__tests__/cluster-consolidation.test.ts +0 -712
- package/src/__tests__/confidence-metadata.test.ts +0 -125
- package/src/__tests__/confidence-tracker.test.ts +0 -249
- package/src/__tests__/conflict-advisor.test.ts +0 -206
- package/src/__tests__/conflict-escalation.test.ts +0 -149
- package/src/__tests__/conflict-lifecycle.test.ts +0 -83
- package/src/__tests__/conflict-output.test.ts +0 -85
- package/src/__tests__/conflict-store.test.ts +0 -374
- package/src/__tests__/consolidation-engine.test.ts +0 -181
- package/src/__tests__/constructive-retrieval.test.ts +0 -509
- package/src/__tests__/context-collapse-renderer.test.ts +0 -214
- package/src/__tests__/context-composer-pins.test.ts +0 -64
- package/src/__tests__/context-composer-task-selection.test.ts +0 -24
- package/src/__tests__/context-reconstructor.test.ts +0 -65
- package/src/__tests__/context-renderer.test.ts +0 -91
- package/src/__tests__/continuity-eval.test.ts +0 -275
- package/src/__tests__/continuity-schema.test.ts +0 -97
- package/src/__tests__/conversation-importer.test.ts +0 -692
- package/src/__tests__/core-recall-eval.test.ts +0 -235
- package/src/__tests__/core-summary.test.ts +0 -109
- package/src/__tests__/data-checkup.test.ts +0 -225
- package/src/__tests__/decay-exemption.test.ts +0 -84
- package/src/__tests__/dedup-actions.test.ts +0 -58
- package/src/__tests__/dedup-guard.test.ts +0 -39
- package/src/__tests__/deterministic-id.test.ts +0 -39
- package/src/__tests__/distill-lock.test.ts +0 -176
- package/src/__tests__/doctor.test.ts +0 -303
- package/src/__tests__/dream-pipeline.test.ts +0 -232
- package/src/__tests__/drill-down.test.ts +0 -108
- package/src/__tests__/embedder.test.ts +0 -122
- package/src/__tests__/emotion-decay.test.ts +0 -46
- package/src/__tests__/emotion-detector.test.ts +0 -56
- package/src/__tests__/emotion-phase1-integration.test.ts +0 -153
- package/src/__tests__/emotion-retrieval.test.ts +0 -56
- package/src/__tests__/entity-resolver.test.ts +0 -58
- package/src/__tests__/ethics-layer.test.ts +0 -476
- package/src/__tests__/event-segmenter.test.ts +0 -153
- package/src/__tests__/feature-flags-integration.test.ts +0 -54
- package/src/__tests__/frequency-tracker.test.ts +0 -113
- package/src/__tests__/graph-export.test.ts +0 -489
- package/src/__tests__/hotness-blend.test.ts +0 -76
- package/src/__tests__/ingest.test.ts +0 -360
- package/src/__tests__/interference-detector.test.ts +0 -203
- package/src/__tests__/kg-extractor.test.ts +0 -184
- package/src/__tests__/kg-store.test.ts +0 -198
- package/src/__tests__/llm-consolidation.test.ts +0 -226
- package/src/__tests__/mcp-efficiency.test.ts +0 -288
- package/src/__tests__/memory-boundaries.test.ts +0 -237
- package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -243
- package/src/__tests__/memory-evolution.test.ts +0 -271
- package/src/__tests__/memory-health-rebalance.test.ts +0 -144
- package/src/__tests__/memory-lint.test.ts +0 -271
- package/src/__tests__/memory-output.test.ts +0 -215
- package/src/__tests__/multi-vector.test.ts +0 -242
- package/src/__tests__/narrative-architecture.test.ts +0 -456
- package/src/__tests__/pending-review.test.ts +0 -77
- package/src/__tests__/post-retrieval-filter.test.ts +0 -178
- package/src/__tests__/ppr-traversal.test.ts +0 -256
- package/src/__tests__/prediction-engine.test.ts +0 -349
- package/src/__tests__/predictive-prospective-memory.test.ts +0 -416
- package/src/__tests__/preference-guard-smoke.test.ts +0 -211
- package/src/__tests__/preference-matcher.test.ts +0 -230
- package/src/__tests__/preference-slots.test.ts +0 -205
- package/src/__tests__/prospective-memory.test.ts +0 -283
- package/src/__tests__/query-entity-detector.test.ts +0 -171
- package/src/__tests__/query-expander-alias.test.ts +0 -89
- package/src/__tests__/query-expander.test.ts +0 -34
- package/src/__tests__/recall-governor.test.ts +0 -227
- package/src/__tests__/recall-mode.test.ts +0 -64
- package/src/__tests__/result-synthesizer.test.ts +0 -198
- package/src/__tests__/resume-context.test.ts +0 -4898
- package/src/__tests__/retrieval-trace.test.ts +0 -213
- package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
- package/src/__tests__/retroactive-boost.test.ts +0 -223
- package/src/__tests__/salience-scoring.test.ts +0 -106
- package/src/__tests__/scope-inventory.test.ts +0 -200
- package/src/__tests__/scope-policy.test.ts +0 -69
- package/src/__tests__/session-checkpoint.test.ts +0 -259
- package/src/__tests__/session-distiller.test.ts +0 -611
- package/src/__tests__/session-gc.test.ts +0 -215
- package/src/__tests__/skill-engine.test.ts +0 -546
- package/src/__tests__/skill-promotion.test.ts +0 -349
- package/src/__tests__/store-scope-audit.test.ts +0 -81
- package/src/__tests__/supersede-chain.test.ts +0 -154
- package/src/__tests__/temporal-parser.test.ts +0 -241
- package/src/__tests__/temporal-validity.test.ts +0 -160
- package/src/__tests__/term-registry.test.ts +0 -121
- package/src/__tests__/tool-output-compressor.test.ts +0 -133
- package/src/__tests__/topic-tag.test.ts +0 -113
- package/src/__tests__/trusted-memory.test.ts +0 -828
- package/src/__tests__/version-manager.test.ts +0 -213
- package/src/__tests__/workflow-observation.test.ts +0 -400
- 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
|
-
```
|