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.
- package/.env +4 -0
- package/CLAUDE.md +7 -3
- package/README.md +202 -130
- package/README_CN.md +263 -190
- package/bin/recallnest.mjs +62 -0
- package/config.json +55 -0
- package/daily-recall.sh +23 -0
- package/integrations/setup-all.sh +31 -0
- package/logs/daily-recall.error.log +0 -0
- package/logs/daily-recall.log +30 -0
- package/logs/ingest-2026-04-11.log +110 -0
- package/logs/ingest-2026-04-12.log +90 -0
- package/logs/launchagent-stderr.log +0 -0
- package/logs/launchagent-stdout.log +0 -0
- package/package.json +10 -3
- package/src/access-tracker.ts +3 -2
- package/src/activity-counter.ts +109 -0
- package/src/adaptive-retrieval.ts +6 -5
- package/src/admission-control.ts +163 -0
- package/src/anchor-generator.ts +80 -0
- package/src/api-server.ts +10 -0
- package/src/audit-log.ts +122 -0
- package/src/auto-gc.ts +98 -29
- package/src/auto-recall.ts +33 -4
- package/src/capture-engine.ts +372 -23
- package/src/cascade-forget.ts +3 -2
- package/src/cli.ts +107 -5
- package/src/compat.ts +22 -0
- package/src/confidence-tracker.ts +210 -47
- package/src/conflict-store.ts +12 -3
- package/src/consolidation-engine.ts +296 -8
- package/src/context-collapse-renderer.ts +222 -0
- package/src/context-composer-stable-selection.ts +5 -1
- package/src/context-composer.ts +239 -4
- package/src/context-reconstructor.ts +404 -0
- package/src/conversation-importer.ts +441 -0
- package/src/data-checkup.ts +232 -0
- package/src/decay-engine.ts +84 -0
- package/src/distill-lock.ts +122 -0
- package/src/doctor.ts +8 -10
- package/src/dream-pipeline.ts +227 -0
- package/src/emotion-detector.ts +90 -0
- package/src/eval.ts +3 -2
- package/src/forget-engine.ts +269 -0
- package/src/frequency-tracker.ts +175 -0
- package/src/graph-export.ts +644 -0
- package/src/ingest.ts +36 -6
- package/src/interference-detector.ts +187 -0
- package/src/kg-extractor.ts +15 -24
- package/src/kg-store.ts +2 -1
- package/src/language-hook.ts +67 -0
- package/src/llm-client.ts +289 -6
- package/src/mcp-server.ts +707 -80
- package/src/memory-assets.ts +22 -5
- package/src/memory-evolution.ts +365 -0
- package/src/memory-lint.ts +429 -0
- package/src/memory-output.ts +69 -0
- package/src/memory-schema.ts +74 -2
- package/src/narrative-schema.ts +79 -0
- package/src/narrative-tagger.ts +164 -0
- package/src/pii-detector.ts +98 -0
- package/src/post-retrieval-filter.ts +76 -0
- package/src/prediction-engine.ts +253 -0
- package/src/preference-slots.ts +65 -2
- package/src/prospective-memory.ts +215 -0
- package/src/query-expander.ts +67 -0
- package/src/recall-governor.ts +183 -0
- package/src/retention-policy.ts +90 -0
- package/src/retriever.ts +828 -55
- package/src/rif.ts +34 -8
- package/src/runtime-config.ts +23 -8
- package/src/scope-inventory.ts +4 -3
- package/src/scope-policy.ts +4 -0
- package/src/session-distiller.ts +307 -0
- package/src/session-output.ts +40 -0
- package/src/session-schema.ts +37 -0
- package/src/session-store.ts +7 -2
- package/src/skill-engine.ts +208 -0
- package/src/skill-promotion.ts +293 -0
- package/src/skill-schema.ts +39 -0
- package/src/store.ts +96 -10
- package/src/topic-tag.ts +66 -0
- package/src/tracker.ts +3 -1
- package/src/ui-server.ts +80 -8
- package/src/workflow-observation-scope-review.ts +2 -1
- package/src/workflow-observation-store.ts +2 -1
- package/src/write-verifier.ts +126 -0
- package/.claude-plugin/CLAUDE.md +0 -29
- package/.claude-plugin/marketplace.json +0 -18
- package/.github/workflows/ci.yml +0 -28
- package/.mcp.json +0 -9
- package/CHANGELOG.md +0 -51
- package/DEMO.md +0 -94
- package/EVAL.md +0 -89
- package/FAILURES.md +0 -75
- package/OPERATIONS.md +0 -59
- package/ROADMAP.md +0 -198
- package/assets/recallnest-workbench.png +0 -0
- package/assets/ui/app.js +0 -807
- package/assets/ui/index.html +0 -154
- package/assets/ui/styles.css +0 -605
- package/assets/wechat-qr.jpg +0 -0
- 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/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/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/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/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/lm +0 -19
- package/plan-v1.2-distributable.md +0 -105
- package/scripts/archive-distilled-junk.ts +0 -116
- 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/distill-facts.ts +0 -305
- package/scripts/health-check.ts +0 -343
- package/scripts/incremental-ingest.sh +0 -71
- 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/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__/auto-consolidation.test.ts +0 -168
- package/src/__tests__/auto-recall.test.ts +0 -131
- package/src/__tests__/batch-dedup.test.ts +0 -65
- package/src/__tests__/capture-engine.test.ts +0 -1142
- 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__/confidence-tracker.test.ts +0 -196
- 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__/context-composer-pins.test.ts +0 -64
- package/src/__tests__/context-composer-task-selection.test.ts +0 -24
- 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__/core-recall-eval.test.ts +0 -235
- package/src/__tests__/core-summary.test.ts +0 -109
- package/src/__tests__/dedup-actions.test.ts +0 -58
- package/src/__tests__/deterministic-id.test.ts +0 -39
- package/src/__tests__/doctor.test.ts +0 -303
- package/src/__tests__/drill-down.test.ts +0 -108
- package/src/__tests__/embedder.test.ts +0 -122
- package/src/__tests__/entity-resolver.test.ts +0 -58
- package/src/__tests__/event-segmenter.test.ts +0 -153
- package/src/__tests__/hotness-blend.test.ts +0 -76
- package/src/__tests__/ingest.test.ts +0 -360
- 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__/memory-boundaries.test.ts +0 -237
- package/src/__tests__/memory-health-rebalance.test.ts +0 -144
- package/src/__tests__/memory-output.test.ts +0 -215
- package/src/__tests__/multi-vector.test.ts +0 -242
- package/src/__tests__/ppr-traversal.test.ts +0 -256
- 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 -106
- package/src/__tests__/prospective-memory.test.ts +0 -283
- package/src/__tests__/query-entity-detector.test.ts +0 -171
- package/src/__tests__/query-expander.test.ts +0 -34
- 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 -4743
- 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__/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-gc.test.ts +0 -215
- package/src/__tests__/store-scope-audit.test.ts +0 -81
- package/src/__tests__/temporal-parser.test.ts +0 -241
- package/src/__tests__/term-registry.test.ts +0 -121
- package/src/__tests__/tool-output-compressor.test.ts +0 -133
- package/src/__tests__/version-manager.test.ts +0 -213
- package/src/__tests__/workflow-observation.test.ts +0 -400
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import type { PersistMemoryDeps } from "./capture-engine.js";
|
|
3
|
+
import { persistMemory } from "./capture-engine.js";
|
|
4
|
+
import type { StoredMemoryRecord } from "./memory-schema.js";
|
|
5
|
+
|
|
6
|
+
export interface NormalizedMessage {
|
|
7
|
+
role: "user" | "assistant" | "system";
|
|
8
|
+
content: string;
|
|
9
|
+
timestamp?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type ConversationFormat =
|
|
13
|
+
| "claude-code"
|
|
14
|
+
| "claude-ai"
|
|
15
|
+
| "chatgpt"
|
|
16
|
+
| "slack"
|
|
17
|
+
| "plaintext";
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Format detection
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
export function detectFormat(content: string): ConversationFormat {
|
|
24
|
+
const trimmed = content.trimStart();
|
|
25
|
+
|
|
26
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(trimmed);
|
|
29
|
+
|
|
30
|
+
if (Array.isArray(parsed)) {
|
|
31
|
+
if (parsed.length > 0 && parsed[0].chat_messages !== undefined) {
|
|
32
|
+
return "claude-ai";
|
|
33
|
+
}
|
|
34
|
+
if (parsed.length > 0 && parsed[0].mapping !== undefined) {
|
|
35
|
+
return "chatgpt";
|
|
36
|
+
}
|
|
37
|
+
if (parsed.length > 0 && parsed[0].ts !== undefined && parsed[0].text !== undefined) {
|
|
38
|
+
return "slack";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
43
|
+
if (parsed.chat_conversations !== undefined) {
|
|
44
|
+
return "claude-ai";
|
|
45
|
+
}
|
|
46
|
+
if (parsed.mapping !== undefined) {
|
|
47
|
+
return "chatgpt";
|
|
48
|
+
}
|
|
49
|
+
if (parsed.messages !== undefined && Array.isArray(parsed.messages)) {
|
|
50
|
+
const first = parsed.messages[0];
|
|
51
|
+
if (first && first.ts !== undefined) {
|
|
52
|
+
return "slack";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// Not valid JSON — fall through
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// JSONL: each line is a JSON object with type field
|
|
62
|
+
const firstLine = trimmed.split("\n")[0]?.trim();
|
|
63
|
+
if (firstLine?.startsWith("{")) {
|
|
64
|
+
try {
|
|
65
|
+
const obj = JSON.parse(firstLine);
|
|
66
|
+
if (obj.type === "human" || obj.type === "assistant" || obj.message !== undefined) {
|
|
67
|
+
return "claude-code";
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
// Not JSONL
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return "plaintext";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Claude Code JSONL normalizer
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
interface ClaudeCodeLine {
|
|
82
|
+
type?: string;
|
|
83
|
+
message?: {
|
|
84
|
+
content?: string | Array<{ type?: string; text?: string }>;
|
|
85
|
+
role?: string;
|
|
86
|
+
};
|
|
87
|
+
timestamp?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function normalizeClaudeCode(content: string): NormalizedMessage[] {
|
|
91
|
+
const messages: NormalizedMessage[] = [];
|
|
92
|
+
|
|
93
|
+
for (const line of content.split("\n")) {
|
|
94
|
+
const trimmed = line.trim();
|
|
95
|
+
if (!trimmed) continue;
|
|
96
|
+
|
|
97
|
+
let parsed: ClaudeCodeLine;
|
|
98
|
+
try {
|
|
99
|
+
parsed = JSON.parse(trimmed);
|
|
100
|
+
} catch {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let role: NormalizedMessage["role"];
|
|
105
|
+
if (parsed.type === "human") {
|
|
106
|
+
role = "user";
|
|
107
|
+
} else if (parsed.type === "assistant") {
|
|
108
|
+
role = "assistant";
|
|
109
|
+
} else if (parsed.message?.role === "user" || parsed.type === "user") {
|
|
110
|
+
role = "user";
|
|
111
|
+
} else if (parsed.message?.role === "assistant") {
|
|
112
|
+
role = "assistant";
|
|
113
|
+
} else {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let text = "";
|
|
118
|
+
const rawContent = parsed.message?.content;
|
|
119
|
+
if (typeof rawContent === "string") {
|
|
120
|
+
text = rawContent;
|
|
121
|
+
} else if (Array.isArray(rawContent)) {
|
|
122
|
+
text = rawContent
|
|
123
|
+
.filter((part): part is { type?: string; text?: string } =>
|
|
124
|
+
typeof part === "object" && part !== null && typeof part.text === "string")
|
|
125
|
+
.map((part) => part.text!)
|
|
126
|
+
.join("\n");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!text.trim()) continue;
|
|
130
|
+
|
|
131
|
+
messages.push({
|
|
132
|
+
role,
|
|
133
|
+
content: text.trim(),
|
|
134
|
+
...(parsed.timestamp ? { timestamp: parsed.timestamp } : {}),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return messages;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// Claude.ai JSON export normalizer
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
interface ClaudeAiConversation {
|
|
146
|
+
chat_messages?: Array<{
|
|
147
|
+
sender?: string;
|
|
148
|
+
text?: string;
|
|
149
|
+
created_at?: string;
|
|
150
|
+
}>;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function normalizeClaudeAi(content: string): NormalizedMessage[] {
|
|
154
|
+
const parsed = JSON.parse(content);
|
|
155
|
+
const messages: NormalizedMessage[] = [];
|
|
156
|
+
|
|
157
|
+
let conversations: ClaudeAiConversation[];
|
|
158
|
+
if (Array.isArray(parsed)) {
|
|
159
|
+
conversations = parsed;
|
|
160
|
+
} else if (parsed.chat_conversations) {
|
|
161
|
+
conversations = parsed.chat_conversations;
|
|
162
|
+
} else {
|
|
163
|
+
conversations = [parsed];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (const conv of conversations) {
|
|
167
|
+
if (!conv.chat_messages) continue;
|
|
168
|
+
for (const msg of conv.chat_messages) {
|
|
169
|
+
let role: NormalizedMessage["role"];
|
|
170
|
+
if (msg.sender === "human") {
|
|
171
|
+
role = "user";
|
|
172
|
+
} else if (msg.sender === "assistant") {
|
|
173
|
+
role = "assistant";
|
|
174
|
+
} else {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const text = msg.text?.trim();
|
|
179
|
+
if (!text) continue;
|
|
180
|
+
|
|
181
|
+
messages.push({
|
|
182
|
+
role,
|
|
183
|
+
content: text,
|
|
184
|
+
...(msg.created_at ? { timestamp: msg.created_at } : {}),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return messages;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// ChatGPT conversations.json normalizer
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
interface ChatGPTMapping {
|
|
197
|
+
[id: string]: {
|
|
198
|
+
id: string;
|
|
199
|
+
parent?: string | null;
|
|
200
|
+
children?: string[];
|
|
201
|
+
message?: {
|
|
202
|
+
author?: { role?: string };
|
|
203
|
+
content?: { parts?: Array<string | Record<string, unknown>> };
|
|
204
|
+
create_time?: number;
|
|
205
|
+
} | null;
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
interface ChatGPTConversation {
|
|
210
|
+
mapping?: ChatGPTMapping;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function normalizeChatGPT(content: string): NormalizedMessage[] {
|
|
214
|
+
const parsed = JSON.parse(content);
|
|
215
|
+
const messages: NormalizedMessage[] = [];
|
|
216
|
+
|
|
217
|
+
const conversations: ChatGPTConversation[] = Array.isArray(parsed) ? parsed : [parsed];
|
|
218
|
+
|
|
219
|
+
for (const conv of conversations) {
|
|
220
|
+
if (!conv.mapping) continue;
|
|
221
|
+
const mapping = conv.mapping;
|
|
222
|
+
|
|
223
|
+
// Find root: node whose parent is null or not in mapping
|
|
224
|
+
let rootId: string | null = null;
|
|
225
|
+
for (const [id, node] of Object.entries(mapping)) {
|
|
226
|
+
if (!node.parent || !mapping[node.parent]) {
|
|
227
|
+
rootId = id;
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!rootId) continue;
|
|
233
|
+
|
|
234
|
+
// BFS to rebuild order
|
|
235
|
+
const queue: string[] = [rootId];
|
|
236
|
+
const visited = new Set<string>();
|
|
237
|
+
|
|
238
|
+
while (queue.length > 0) {
|
|
239
|
+
const nodeId = queue.shift()!;
|
|
240
|
+
if (visited.has(nodeId)) continue;
|
|
241
|
+
visited.add(nodeId);
|
|
242
|
+
|
|
243
|
+
const node = mapping[nodeId];
|
|
244
|
+
if (!node) continue;
|
|
245
|
+
|
|
246
|
+
if (node.message?.author?.role && node.message.content?.parts) {
|
|
247
|
+
const authorRole = node.message.author.role;
|
|
248
|
+
let role: NormalizedMessage["role"] | null = null;
|
|
249
|
+
if (authorRole === "user") role = "user";
|
|
250
|
+
else if (authorRole === "assistant") role = "assistant";
|
|
251
|
+
else if (authorRole === "system") role = "system";
|
|
252
|
+
|
|
253
|
+
if (role) {
|
|
254
|
+
const text = node.message.content.parts
|
|
255
|
+
.filter((part): part is string => typeof part === "string")
|
|
256
|
+
.join("\n")
|
|
257
|
+
.trim();
|
|
258
|
+
|
|
259
|
+
if (text) {
|
|
260
|
+
const msg: NormalizedMessage = { role, content: text };
|
|
261
|
+
if (node.message.create_time) {
|
|
262
|
+
msg.timestamp = new Date(node.message.create_time * 1000).toISOString();
|
|
263
|
+
}
|
|
264
|
+
messages.push(msg);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (node.children) {
|
|
270
|
+
for (const childId of node.children) {
|
|
271
|
+
queue.push(childId);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return messages;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
// Slack JSON export normalizer
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
interface SlackMessage {
|
|
285
|
+
user?: string;
|
|
286
|
+
bot_id?: string;
|
|
287
|
+
text?: string;
|
|
288
|
+
ts?: string;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function normalizeSlack(content: string): NormalizedMessage[] {
|
|
292
|
+
const parsed = JSON.parse(content);
|
|
293
|
+
const messages: NormalizedMessage[] = [];
|
|
294
|
+
|
|
295
|
+
let rawMessages: SlackMessage[];
|
|
296
|
+
if (Array.isArray(parsed)) {
|
|
297
|
+
rawMessages = parsed;
|
|
298
|
+
} else if (parsed.messages && Array.isArray(parsed.messages)) {
|
|
299
|
+
rawMessages = parsed.messages;
|
|
300
|
+
} else {
|
|
301
|
+
return messages;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
for (const msg of rawMessages) {
|
|
305
|
+
const text = msg.text?.trim();
|
|
306
|
+
if (!text) continue;
|
|
307
|
+
|
|
308
|
+
const role: NormalizedMessage["role"] = msg.bot_id ? "assistant" : "user";
|
|
309
|
+
|
|
310
|
+
const result: NormalizedMessage = { role, content: text };
|
|
311
|
+
if (msg.ts) {
|
|
312
|
+
result.timestamp = new Date(parseFloat(msg.ts) * 1000).toISOString();
|
|
313
|
+
}
|
|
314
|
+
messages.push(result);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return messages;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
// Plaintext normalizer
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
const ROLE_PREFIXES: Array<[RegExp, NormalizedMessage["role"]]> = [
|
|
325
|
+
[/^User:\s*/i, "user"],
|
|
326
|
+
[/^Human:\s*/i, "user"],
|
|
327
|
+
[/^Assistant:\s*/i, "assistant"],
|
|
328
|
+
[/^AI:\s*/i, "assistant"],
|
|
329
|
+
[/^System:\s*/i, "system"],
|
|
330
|
+
];
|
|
331
|
+
|
|
332
|
+
export function normalizePlaintext(content: string): NormalizedMessage[] {
|
|
333
|
+
const messages: NormalizedMessage[] = [];
|
|
334
|
+
let currentRole: NormalizedMessage["role"] | null = null;
|
|
335
|
+
let currentLines: string[] = [];
|
|
336
|
+
|
|
337
|
+
function flush() {
|
|
338
|
+
if (currentRole && currentLines.length > 0) {
|
|
339
|
+
const text = currentLines.join("\n").trim();
|
|
340
|
+
if (text) {
|
|
341
|
+
messages.push({ role: currentRole, content: text });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
currentLines = [];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
for (const line of content.split("\n")) {
|
|
348
|
+
let matched = false;
|
|
349
|
+
for (const [pattern, role] of ROLE_PREFIXES) {
|
|
350
|
+
const match = line.match(pattern);
|
|
351
|
+
if (match) {
|
|
352
|
+
flush();
|
|
353
|
+
currentRole = role;
|
|
354
|
+
const remainder = line.slice(match[0].length).trim();
|
|
355
|
+
if (remainder) currentLines.push(remainder);
|
|
356
|
+
matched = true;
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (!matched && currentRole) {
|
|
361
|
+
currentLines.push(line);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
flush();
|
|
365
|
+
|
|
366
|
+
return messages;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ---------------------------------------------------------------------------
|
|
370
|
+
// Unified normalize dispatcher
|
|
371
|
+
// ---------------------------------------------------------------------------
|
|
372
|
+
|
|
373
|
+
export function normalizeConversation(content: string, format: ConversationFormat): NormalizedMessage[] {
|
|
374
|
+
switch (format) {
|
|
375
|
+
case "claude-code":
|
|
376
|
+
return normalizeClaudeCode(content);
|
|
377
|
+
case "claude-ai":
|
|
378
|
+
return normalizeClaudeAi(content);
|
|
379
|
+
case "chatgpt":
|
|
380
|
+
return normalizeChatGPT(content);
|
|
381
|
+
case "slack":
|
|
382
|
+
return normalizeSlack(content);
|
|
383
|
+
case "plaintext":
|
|
384
|
+
return normalizePlaintext(content);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
// Batch ingest: normalized messages -> persistMemory
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
|
|
392
|
+
export interface ImportResult {
|
|
393
|
+
total: number;
|
|
394
|
+
stored: number;
|
|
395
|
+
rejected: number;
|
|
396
|
+
errors: string[];
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export async function importConversation(
|
|
400
|
+
deps: PersistMemoryDeps,
|
|
401
|
+
filePath: string,
|
|
402
|
+
scope: string,
|
|
403
|
+
format: ConversationFormat | "auto",
|
|
404
|
+
): Promise<ImportResult> {
|
|
405
|
+
const content = readFileSync(filePath, "utf-8");
|
|
406
|
+
const resolvedFormat = format === "auto" ? detectFormat(content) : format;
|
|
407
|
+
const messages = normalizeConversation(content, resolvedFormat);
|
|
408
|
+
|
|
409
|
+
return ingestNormalizedMessages(deps, messages, scope);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export async function ingestNormalizedMessages(
|
|
413
|
+
deps: PersistMemoryDeps,
|
|
414
|
+
messages: NormalizedMessage[],
|
|
415
|
+
scope: string,
|
|
416
|
+
): Promise<ImportResult> {
|
|
417
|
+
const result: ImportResult = { total: messages.length, stored: 0, rejected: 0, errors: [] };
|
|
418
|
+
|
|
419
|
+
for (const msg of messages) {
|
|
420
|
+
try {
|
|
421
|
+
const stored: StoredMemoryRecord = await persistMemory(deps, {
|
|
422
|
+
text: msg.content,
|
|
423
|
+
category: "events" as const,
|
|
424
|
+
importance: 0.7,
|
|
425
|
+
scope,
|
|
426
|
+
source: "conversation_import" as const,
|
|
427
|
+
tags: [`role:${msg.role}`, ...(msg.timestamp ? [`ts:${msg.timestamp}`] : [])],
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
if (stored.disposition === "rejected") {
|
|
431
|
+
result.rejected++;
|
|
432
|
+
} else {
|
|
433
|
+
result.stored++;
|
|
434
|
+
}
|
|
435
|
+
} catch (err) {
|
|
436
|
+
result.errors.push(err instanceof Error ? err.message : String(err));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return result;
|
|
441
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LC-P4: Data Checkup Engine
|
|
3
|
+
*
|
|
4
|
+
* Health checks for the memory database, inspired by lossless-claw's
|
|
5
|
+
* IntegrityChecker. Pure read-only diagnostics — never modifies data.
|
|
6
|
+
*
|
|
7
|
+
* Checks:
|
|
8
|
+
* 1. Vector dimension consistency
|
|
9
|
+
* 2. Orphan memories (scope missing or empty)
|
|
10
|
+
* 3. Tier distribution health
|
|
11
|
+
* 4. Conflict backlog
|
|
12
|
+
* 5. Version group integrity
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { MemoryEntry, MemoryStore } from "./store.js";
|
|
16
|
+
import { resolveTier, type MemoryTier } from "./decay-engine.js";
|
|
17
|
+
import { measureInterferenceDensity } from "./interference-detector.js";
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Types
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
export interface CheckResult {
|
|
24
|
+
name: string;
|
|
25
|
+
status: "ok" | "warning" | "error";
|
|
26
|
+
detail: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface CheckupReport {
|
|
30
|
+
checks: CheckResult[];
|
|
31
|
+
totalEntries: number;
|
|
32
|
+
timestamp: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Individual checks
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
/** Check 1: All vectors should have the same dimension. */
|
|
40
|
+
function checkVectorDimensions(entries: MemoryEntry[]): CheckResult {
|
|
41
|
+
const dims = new Map<number, number>(); // dim → count
|
|
42
|
+
for (const e of entries) {
|
|
43
|
+
const d = e.vector?.length ?? 0;
|
|
44
|
+
dims.set(d, (dims.get(d) ?? 0) + 1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (dims.size <= 1) {
|
|
48
|
+
const dim = dims.keys().next().value ?? 0;
|
|
49
|
+
return { name: "vector_dimensions", status: "ok", detail: `All ${entries.length} entries have dimension ${dim}` };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const sorted = [...dims.entries()].sort((a, b) => b[1] - a[1]);
|
|
53
|
+
const expected = sorted[0][0];
|
|
54
|
+
const mismatched = sorted.slice(1).reduce((sum, [, c]) => sum + c, 0);
|
|
55
|
+
return {
|
|
56
|
+
name: "vector_dimensions",
|
|
57
|
+
status: "error",
|
|
58
|
+
detail: `${mismatched} entries have wrong dimension (expected ${expected}): ${sorted.map(([d, c]) => `dim=${d}: ${c}`).join(", ")}`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Check 2: Memories with missing/empty scope. */
|
|
63
|
+
function checkOrphanMemories(entries: MemoryEntry[]): CheckResult {
|
|
64
|
+
const orphans = entries.filter(e => !e.scope || e.scope.trim() === "" || e.scope === "__schema__");
|
|
65
|
+
if (orphans.length === 0) {
|
|
66
|
+
return { name: "orphan_memories", status: "ok", detail: "No orphan memories found" };
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
name: "orphan_memories",
|
|
70
|
+
status: orphans.length > 10 ? "error" : "warning",
|
|
71
|
+
detail: `${orphans.length} memories with missing/empty/schema scope (IDs: ${orphans.slice(0, 5).map(e => e.id.slice(0, 8)).join(", ")}${orphans.length > 5 ? "..." : ""})`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Check 3: Tier distribution — core should not exceed 500, peripheral should not exceed 70%. */
|
|
76
|
+
function checkTierDistribution(entries: MemoryEntry[]): CheckResult {
|
|
77
|
+
const tierCounts: Record<MemoryTier, number> = { core: 0, working: 0, peripheral: 0 };
|
|
78
|
+
for (const e of entries) {
|
|
79
|
+
const tier = resolveTier(e.metadata);
|
|
80
|
+
tierCounts[tier]++;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const total = entries.length;
|
|
84
|
+
const issues: string[] = [];
|
|
85
|
+
|
|
86
|
+
if (tierCounts.core > 500) {
|
|
87
|
+
issues.push(`core tier has ${tierCounts.core} entries (max recommended: 500)`);
|
|
88
|
+
}
|
|
89
|
+
if (total >= 20 && tierCounts.peripheral / total > 0.7) {
|
|
90
|
+
const pct = Math.round(tierCounts.peripheral / total * 100);
|
|
91
|
+
issues.push(`peripheral tier is ${pct}% of total (recommended: ≤70%)`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const dist = `core=${tierCounts.core}, working=${tierCounts.working}, peripheral=${tierCounts.peripheral}`;
|
|
95
|
+
if (issues.length === 0) {
|
|
96
|
+
return { name: "tier_distribution", status: "ok", detail: `Healthy distribution: ${dist}` };
|
|
97
|
+
}
|
|
98
|
+
return { name: "tier_distribution", status: "warning", detail: `${dist} — ${issues.join("; ")}` };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Check 4: Open conflict backlog. */
|
|
102
|
+
function checkConflictBacklog(openConflictCount: number): CheckResult {
|
|
103
|
+
if (openConflictCount === 0) {
|
|
104
|
+
return { name: "conflict_backlog", status: "ok", detail: "No open conflicts" };
|
|
105
|
+
}
|
|
106
|
+
const status = openConflictCount > 20 ? "error" : openConflictCount > 5 ? "warning" : "ok";
|
|
107
|
+
return { name: "conflict_backlog", status, detail: `${openConflictCount} open conflicts` };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Check 5: Version group integrity — members should reference the same group ID. */
|
|
111
|
+
function checkVersionGroups(entries: MemoryEntry[]): CheckResult {
|
|
112
|
+
const groups = new Map<string, { ids: string[]; ranks: number[] }>();
|
|
113
|
+
|
|
114
|
+
for (const e of entries) {
|
|
115
|
+
if (!e.metadata) continue;
|
|
116
|
+
try {
|
|
117
|
+
const meta = JSON.parse(e.metadata);
|
|
118
|
+
const group = meta.version_group;
|
|
119
|
+
if (typeof group !== "string") continue;
|
|
120
|
+
if (!groups.has(group)) groups.set(group, { ids: [], ranks: [] });
|
|
121
|
+
const g = groups.get(group)!;
|
|
122
|
+
g.ids.push(e.id);
|
|
123
|
+
g.ranks.push(typeof meta.version_rank === "number" ? meta.version_rank : -1);
|
|
124
|
+
} catch { /* skip */ }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (groups.size === 0) {
|
|
128
|
+
return { name: "version_groups", status: "ok", detail: "No version groups found" };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const issues: string[] = [];
|
|
132
|
+
for (const [groupId, { ids, ranks }] of groups) {
|
|
133
|
+
// Single-member group is suspicious
|
|
134
|
+
if (ids.length < 2) {
|
|
135
|
+
issues.push(`group ${groupId.slice(0, 8)} has only ${ids.length} member`);
|
|
136
|
+
}
|
|
137
|
+
// Check for missing ranks
|
|
138
|
+
const missingRanks = ranks.filter(r => r < 0).length;
|
|
139
|
+
if (missingRanks > 0) {
|
|
140
|
+
issues.push(`group ${groupId.slice(0, 8)} has ${missingRanks} members with missing rank`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (issues.length === 0) {
|
|
145
|
+
return { name: "version_groups", status: "ok", detail: `${groups.size} version groups, all healthy` };
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
name: "version_groups",
|
|
149
|
+
status: issues.length > 3 ? "error" : "warning",
|
|
150
|
+
detail: `${groups.size} groups, ${issues.length} issue(s): ${issues.slice(0, 3).join("; ")}${issues.length > 3 ? "..." : ""}`,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Check 6 (F2): Interference density — detect semantic clustering pressure. */
|
|
155
|
+
function checkInterferenceDensity(entries: MemoryEntry[]): CheckResult {
|
|
156
|
+
// Only check entries with vectors (skip schema/empty entries)
|
|
157
|
+
const withVectors = entries.filter(e => e.vector && e.vector.length > 0);
|
|
158
|
+
if (withVectors.length < 10) {
|
|
159
|
+
return { name: "interference_density", status: "ok", detail: "Too few entries for interference analysis" };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Sample up to 200 entries for performance (interference detection is O(n²))
|
|
163
|
+
const sample = withVectors.length > 200
|
|
164
|
+
? withVectors.sort(() => Math.random() - 0.5).slice(0, 200)
|
|
165
|
+
: withVectors;
|
|
166
|
+
|
|
167
|
+
const density = measureInterferenceDensity(sample);
|
|
168
|
+
const issues: string[] = [];
|
|
169
|
+
|
|
170
|
+
if (density.highRiskCount > sample.length * 0.2) {
|
|
171
|
+
issues.push(`${density.highRiskCount} high-risk entries (>${Math.round(sample.length * 0.2)} threshold)`);
|
|
172
|
+
}
|
|
173
|
+
if (density.avgClusterSize > 5) {
|
|
174
|
+
issues.push(`avg cluster size ${density.avgClusterSize.toFixed(1)} (recommended: ≤5)`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const detail = `${density.clusterCount} clusters, avg size ${density.avgClusterSize.toFixed(1)}, ${density.highRiskCount} high-risk`;
|
|
178
|
+
if (issues.length === 0) {
|
|
179
|
+
return { name: "interference_density", status: "ok", detail: `Healthy: ${detail}` };
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
name: "interference_density",
|
|
183
|
+
status: issues.length > 1 ? "error" : "warning",
|
|
184
|
+
detail: `${detail} — ${issues.join("; ")}`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// Main
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
export interface CheckupDeps {
|
|
193
|
+
store: Pick<MemoryStore, "list" | "stats">;
|
|
194
|
+
openConflictCount: number;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function runDataCheckup(deps: CheckupDeps): Promise<CheckupReport> {
|
|
198
|
+
const entries = await deps.store.list(undefined, undefined, 10000, 0);
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
checks: [
|
|
202
|
+
checkVectorDimensions(entries),
|
|
203
|
+
checkOrphanMemories(entries),
|
|
204
|
+
checkTierDistribution(entries),
|
|
205
|
+
checkConflictBacklog(deps.openConflictCount),
|
|
206
|
+
checkVersionGroups(entries),
|
|
207
|
+
checkInterferenceDensity(entries),
|
|
208
|
+
],
|
|
209
|
+
totalEntries: entries.length,
|
|
210
|
+
timestamp: new Date().toISOString(),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function formatCheckupReport(report: CheckupReport): string {
|
|
215
|
+
const lines = [
|
|
216
|
+
`Data Checkup Report (${report.timestamp})`,
|
|
217
|
+
`Total entries scanned: ${report.totalEntries}`,
|
|
218
|
+
"",
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
for (const check of report.checks) {
|
|
222
|
+
const icon = check.status === "ok" ? "[OK]" : check.status === "warning" ? "[WARN]" : "[ERR]";
|
|
223
|
+
lines.push(`${icon} ${check.name}: ${check.detail}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const errorCount = report.checks.filter(c => c.status === "error").length;
|
|
227
|
+
const warnCount = report.checks.filter(c => c.status === "warning").length;
|
|
228
|
+
lines.push("");
|
|
229
|
+
lines.push(`Summary: ${errorCount} error(s), ${warnCount} warning(s), ${report.checks.length - errorCount - warnCount} ok`);
|
|
230
|
+
|
|
231
|
+
return lines.join("\n");
|
|
232
|
+
}
|