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,429 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Lint Engine — Content Quality Checker
|
|
3
|
+
*
|
|
4
|
+
* A read-only quality checker for memory content. Separate from data-checkup.ts
|
|
5
|
+
* which checks infrastructure integrity (vector dims, tier distribution, etc.),
|
|
6
|
+
* Memory Lint checks *content* quality:
|
|
7
|
+
*
|
|
8
|
+
* 1. Contradictions — memories that say opposite things about the same topic
|
|
9
|
+
* 2. Duplicates — near-identical memories by vector cosine similarity
|
|
10
|
+
* 3. Stale — memories never or rarely accessed and old enough to review
|
|
11
|
+
* 4. Orphans — memories with missing scope or broken consolidation links
|
|
12
|
+
*
|
|
13
|
+
* Produces a health score (0-100) and a human-readable report.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { MemoryEntry, MemoryStore } from "./store.js";
|
|
17
|
+
import { cosineSimilarity } from "./multi-vector.js";
|
|
18
|
+
import { parseEvolution, isActiveMemory } from "./memory-evolution.js";
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Types
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
export type LintSeverity = "info" | "warning" | "error";
|
|
25
|
+
|
|
26
|
+
export interface LintFinding {
|
|
27
|
+
check: string; // e.g., "contradiction", "duplicate", "stale", "orphan"
|
|
28
|
+
severity: LintSeverity;
|
|
29
|
+
detail: string;
|
|
30
|
+
memoryIds: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface MemoryLintReport {
|
|
34
|
+
findings: LintFinding[];
|
|
35
|
+
healthScore: number; // 0-100
|
|
36
|
+
totalScanned: number;
|
|
37
|
+
timestamp: string;
|
|
38
|
+
summary: {
|
|
39
|
+
contradictions: number;
|
|
40
|
+
duplicates: number;
|
|
41
|
+
staleMemories: number;
|
|
42
|
+
orphans: number;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface LintDeps {
|
|
47
|
+
store: Pick<MemoryStore, "list">;
|
|
48
|
+
scope?: string;
|
|
49
|
+
verbose?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Constants
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
/** Maximum entries to compare per scope+category group (performance guard). */
|
|
57
|
+
const MAX_ENTRIES_PER_GROUP = 100;
|
|
58
|
+
|
|
59
|
+
/** Cosine similarity threshold for duplicate detection. */
|
|
60
|
+
const DUPLICATE_THRESHOLD = 0.92;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Minimum vector similarity to even consider two entries as potential contradictions.
|
|
64
|
+
* Real contradictions are about the SAME topic but say opposite things,
|
|
65
|
+
* so they should have moderate semantic similarity.
|
|
66
|
+
*/
|
|
67
|
+
const CONTRADICTION_SIMILARITY_FLOOR = 0.45;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Categories where contradictions are meaningful.
|
|
71
|
+
* Append-only categories (events, cases) naturally contain "opposite" entries
|
|
72
|
+
* from different points in time — those are not contradictions.
|
|
73
|
+
*/
|
|
74
|
+
const CONTRADICTION_CATEGORIES = new Set(["profile", "preferences", "entities", "patterns"]);
|
|
75
|
+
|
|
76
|
+
/** Entries not accessed in this many days are candidates for staleness. */
|
|
77
|
+
const STALE_DAYS = 90;
|
|
78
|
+
|
|
79
|
+
const STALE_MS = STALE_DAYS * 24 * 60 * 60 * 1000;
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Contradiction Detection (inline copy from consolidation-engine.ts)
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Heuristic contradiction detection between two memory texts.
|
|
87
|
+
* Checks for negation patterns and requires at least one shared significant
|
|
88
|
+
* term to reduce false positives.
|
|
89
|
+
*/
|
|
90
|
+
function detectContradiction(textA: string, textB: string): boolean {
|
|
91
|
+
const a = textA.toLowerCase();
|
|
92
|
+
const b = textB.toLowerCase();
|
|
93
|
+
|
|
94
|
+
const negationPairs: [RegExp, RegExp][] = [
|
|
95
|
+
[/\bnot\b/, /\b(?:always|must|should|is|are|was|were)\b/],
|
|
96
|
+
[/\bnever\b/, /\b(?:always|every|each)\b/],
|
|
97
|
+
[/\bdisable/, /\benable/],
|
|
98
|
+
[/不要|不用|别/, /必须|一定|总是/],
|
|
99
|
+
[/从不/, /每次|总是|一直/],
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
for (const [negRe, posRe] of negationPairs) {
|
|
103
|
+
if ((negRe.test(a) && posRe.test(b)) || (negRe.test(b) && posRe.test(a))) {
|
|
104
|
+
// Require at least one shared significant term to reduce false positives
|
|
105
|
+
const wordsA = new Set(a.split(/\s+/).filter(w => w.length > 3));
|
|
106
|
+
const wordsB = new Set(b.split(/\s+/).filter(w => w.length > 3));
|
|
107
|
+
for (const w of wordsA) {
|
|
108
|
+
if (wordsB.has(w)) return true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Helpers
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
/** Truncate text for display in findings. */
|
|
120
|
+
function truncate(text: string, maxLen = 60): string {
|
|
121
|
+
if (text.length <= maxLen) return text;
|
|
122
|
+
return text.slice(0, maxLen - 3) + "...";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Group entries by scope + category key.
|
|
127
|
+
* Within each group, sort by importance descending and cap at MAX_ENTRIES_PER_GROUP.
|
|
128
|
+
*/
|
|
129
|
+
function groupEntries(entries: MemoryEntry[]): Map<string, MemoryEntry[]> {
|
|
130
|
+
const groups = new Map<string, MemoryEntry[]>();
|
|
131
|
+
|
|
132
|
+
for (const entry of entries) {
|
|
133
|
+
const key = `${entry.scope ?? ""}::${entry.category}`;
|
|
134
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
135
|
+
groups.get(key)!.push(entry);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Sort each group by importance descending, cap at limit
|
|
139
|
+
for (const [key, group] of groups) {
|
|
140
|
+
group.sort((a, b) => b.importance - a.importance);
|
|
141
|
+
if (group.length > MAX_ENTRIES_PER_GROUP) {
|
|
142
|
+
groups.set(key, group.slice(0, MAX_ENTRIES_PER_GROUP));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return groups;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Check 1: Contradictions
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
function findContradictions(entries: MemoryEntry[]): LintFinding[] {
|
|
154
|
+
const findings: LintFinding[] = [];
|
|
155
|
+
|
|
156
|
+
// Only check merge-type categories where contradictions are meaningful
|
|
157
|
+
const eligible = entries.filter(e => CONTRADICTION_CATEGORIES.has(e.category));
|
|
158
|
+
const groups = groupEntries(eligible);
|
|
159
|
+
|
|
160
|
+
for (const [, group] of groups) {
|
|
161
|
+
if (group.length < 2) continue;
|
|
162
|
+
|
|
163
|
+
for (let i = 0; i < group.length; i++) {
|
|
164
|
+
for (let j = i + 1; j < group.length; j++) {
|
|
165
|
+
// Pre-filter: entries must be about the same topic (moderate vector similarity)
|
|
166
|
+
const sim = cosineSimilarity(group[i].vector, group[j].vector);
|
|
167
|
+
if (sim < CONTRADICTION_SIMILARITY_FLOOR) continue;
|
|
168
|
+
|
|
169
|
+
if (detectContradiction(group[i].text, group[j].text)) {
|
|
170
|
+
findings.push({
|
|
171
|
+
check: "contradiction",
|
|
172
|
+
severity: "warning",
|
|
173
|
+
detail: `"${truncate(group[i].text)}" vs "${truncate(group[j].text)}"`,
|
|
174
|
+
memoryIds: [group[i].id, group[j].id],
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return findings;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Check 2: Duplicates
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
function findDuplicates(entries: MemoryEntry[]): LintFinding[] {
|
|
189
|
+
const findings: LintFinding[] = [];
|
|
190
|
+
const groups = groupEntries(entries);
|
|
191
|
+
|
|
192
|
+
for (const [, group] of groups) {
|
|
193
|
+
if (group.length < 2) continue;
|
|
194
|
+
|
|
195
|
+
for (let i = 0; i < group.length; i++) {
|
|
196
|
+
for (let j = i + 1; j < group.length; j++) {
|
|
197
|
+
const sim = cosineSimilarity(group[i].vector, group[j].vector);
|
|
198
|
+
if (sim >= DUPLICATE_THRESHOLD) {
|
|
199
|
+
findings.push({
|
|
200
|
+
check: "duplicate",
|
|
201
|
+
severity: "warning",
|
|
202
|
+
detail: `"${truncate(group[i].text)}" -- ${(sim * 100).toFixed(1)}% similar [${truncate(group[j].text)}]`,
|
|
203
|
+
memoryIds: [group[i].id, group[j].id],
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return findings;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// Check 3: Stale Memories
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
function findStaleMemories(entries: MemoryEntry[]): LintFinding[] {
|
|
218
|
+
const findings: LintFinding[] = [];
|
|
219
|
+
const now = Date.now();
|
|
220
|
+
|
|
221
|
+
for (const entry of entries) {
|
|
222
|
+
const evo = parseEvolution(entry.metadata, entry.timestamp);
|
|
223
|
+
|
|
224
|
+
// Stale = entry itself is old enough AND (lastAccessedAt is null or > 90 days ago) AND accessCount <= 1
|
|
225
|
+
const entryAge = now - entry.timestamp;
|
|
226
|
+
if (entryAge < STALE_MS) continue; // too new to be considered stale
|
|
227
|
+
|
|
228
|
+
const lastAccess = evo.lastAccessedAt;
|
|
229
|
+
const isOldAccess = lastAccess === null || (now - lastAccess > STALE_MS);
|
|
230
|
+
|
|
231
|
+
if (isOldAccess && evo.accessCount <= 1) {
|
|
232
|
+
const ageDays = Math.floor((now - entry.timestamp) / (24 * 60 * 60 * 1000));
|
|
233
|
+
findings.push({
|
|
234
|
+
check: "stale",
|
|
235
|
+
severity: "info",
|
|
236
|
+
detail: `${ageDays}d old, ${evo.accessCount} access(es): "${truncate(entry.text, 50)}"`,
|
|
237
|
+
memoryIds: [entry.id],
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return findings;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// Check 4: Orphans
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
function findOrphans(entries: MemoryEntry[]): LintFinding[] {
|
|
250
|
+
const findings: LintFinding[] = [];
|
|
251
|
+
|
|
252
|
+
// Build set of all loaded IDs for consolidation link checking
|
|
253
|
+
const allIds = new Set(entries.map(e => e.id));
|
|
254
|
+
|
|
255
|
+
for (const entry of entries) {
|
|
256
|
+
// Missing or schema scope
|
|
257
|
+
if (!entry.scope || entry.scope.trim() === "" || entry.scope === "__schema__") {
|
|
258
|
+
findings.push({
|
|
259
|
+
check: "orphan",
|
|
260
|
+
severity: "info",
|
|
261
|
+
detail: `Missing/empty scope: "${truncate(entry.text, 50)}"`,
|
|
262
|
+
memoryIds: [entry.id],
|
|
263
|
+
});
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Broken consolidation link
|
|
268
|
+
const evo = parseEvolution(entry.metadata, entry.timestamp);
|
|
269
|
+
if (evo.consolidatedInto && !allIds.has(evo.consolidatedInto)) {
|
|
270
|
+
findings.push({
|
|
271
|
+
check: "orphan",
|
|
272
|
+
severity: "warning",
|
|
273
|
+
detail: `Broken consolidation link -> ${evo.consolidatedInto.slice(0, 12)}...: "${truncate(entry.text, 40)}"`,
|
|
274
|
+
memoryIds: [entry.id],
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return findings;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
// Health Score
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Compute a 0-100 health score from finding counts.
|
|
288
|
+
*
|
|
289
|
+
* Weights:
|
|
290
|
+
* - Contradictions: -10 each (most dangerous, conflicting guidance)
|
|
291
|
+
* - Duplicates: -5 each (waste and confusion risk)
|
|
292
|
+
* - Stale: -0.5 each (minor, many are expected)
|
|
293
|
+
* - Orphans: -3 each (moderate, broken references)
|
|
294
|
+
*/
|
|
295
|
+
export function computeHealthScore(
|
|
296
|
+
summary: MemoryLintReport["summary"],
|
|
297
|
+
_total: number,
|
|
298
|
+
): number {
|
|
299
|
+
const penalty =
|
|
300
|
+
summary.contradictions * 10 +
|
|
301
|
+
summary.duplicates * 5 +
|
|
302
|
+
Math.floor(summary.staleMemories * 0.5) +
|
|
303
|
+
summary.orphans * 3;
|
|
304
|
+
|
|
305
|
+
return Math.max(0, Math.min(100, 100 - penalty));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
// Main
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Run all memory lint checks and produce a report.
|
|
314
|
+
*
|
|
315
|
+
* @param deps.store - Memory store (only `list` is needed)
|
|
316
|
+
* @param deps.scope - Optional scope filter; undefined = all scopes
|
|
317
|
+
* @param deps.verbose - Reserved for future use
|
|
318
|
+
*/
|
|
319
|
+
export async function runMemoryLint(deps: LintDeps): Promise<MemoryLintReport> {
|
|
320
|
+
const scopeFilter = deps.scope ? [deps.scope] : undefined;
|
|
321
|
+
const allEntries = await deps.store.list(scopeFilter, undefined, 10000, 0);
|
|
322
|
+
|
|
323
|
+
// Filter to active entries only
|
|
324
|
+
const entries = allEntries.filter(e => isActiveMemory(e.metadata));
|
|
325
|
+
|
|
326
|
+
// Run all checks
|
|
327
|
+
const contradictions = findContradictions(entries);
|
|
328
|
+
const duplicates = findDuplicates(entries);
|
|
329
|
+
const stale = findStaleMemories(entries);
|
|
330
|
+
const orphans = findOrphans(entries);
|
|
331
|
+
|
|
332
|
+
const findings = [...contradictions, ...duplicates, ...stale, ...orphans];
|
|
333
|
+
|
|
334
|
+
const summary = {
|
|
335
|
+
contradictions: contradictions.length,
|
|
336
|
+
duplicates: duplicates.length,
|
|
337
|
+
staleMemories: stale.length,
|
|
338
|
+
orphans: orphans.length,
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
findings,
|
|
343
|
+
healthScore: computeHealthScore(summary, entries.length),
|
|
344
|
+
totalScanned: entries.length,
|
|
345
|
+
timestamp: new Date().toISOString(),
|
|
346
|
+
summary,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
// Formatter
|
|
352
|
+
// ---------------------------------------------------------------------------
|
|
353
|
+
|
|
354
|
+
/** Format a lint report as human-readable text. */
|
|
355
|
+
export function formatMemoryLintReport(report: MemoryLintReport): string {
|
|
356
|
+
const lines: string[] = [];
|
|
357
|
+
|
|
358
|
+
// All clear case
|
|
359
|
+
if (report.findings.length === 0) {
|
|
360
|
+
lines.push("Memory Lint: All Clear!");
|
|
361
|
+
lines.push(`Scanned: ${report.totalScanned} active memories`);
|
|
362
|
+
lines.push(`Health Score: ${report.healthScore}/100`);
|
|
363
|
+
return lines.join("\n");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Header
|
|
367
|
+
lines.push(`Memory Lint Report (${report.timestamp})`);
|
|
368
|
+
lines.push("=".repeat(44));
|
|
369
|
+
lines.push(`Scanned: ${report.totalScanned} active memories`);
|
|
370
|
+
lines.push("");
|
|
371
|
+
|
|
372
|
+
// Contradictions
|
|
373
|
+
if (report.summary.contradictions > 0) {
|
|
374
|
+
const items = report.findings.filter(f => f.check === "contradiction");
|
|
375
|
+
lines.push(`Contradictions (${items.length}):`);
|
|
376
|
+
for (const f of items) {
|
|
377
|
+
lines.push(` - ${f.detail} [${f.memoryIds.map(id => id.slice(0, 8)).join(", ")}]`);
|
|
378
|
+
}
|
|
379
|
+
lines.push("");
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Duplicates
|
|
383
|
+
if (report.summary.duplicates > 0) {
|
|
384
|
+
const items = report.findings.filter(f => f.check === "duplicate");
|
|
385
|
+
lines.push(`Duplicates (${items.length}):`);
|
|
386
|
+
for (const f of items) {
|
|
387
|
+
lines.push(` - ${f.detail} [${f.memoryIds.map(id => id.slice(0, 8)).join(", ")}]`);
|
|
388
|
+
}
|
|
389
|
+
lines.push("");
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Stale
|
|
393
|
+
if (report.summary.staleMemories > 0) {
|
|
394
|
+
const items = report.findings.filter(f => f.check === "stale");
|
|
395
|
+
lines.push(`Stale (${items.length}):`);
|
|
396
|
+
if (items.length <= 5) {
|
|
397
|
+
for (const f of items) {
|
|
398
|
+
lines.push(` - ${f.detail}`);
|
|
399
|
+
}
|
|
400
|
+
} else {
|
|
401
|
+
// Summarize when many
|
|
402
|
+
lines.push(` - ${items.length} memories not accessed in ${STALE_DAYS}+ days`);
|
|
403
|
+
}
|
|
404
|
+
lines.push("");
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Orphans
|
|
408
|
+
if (report.summary.orphans > 0) {
|
|
409
|
+
const items = report.findings.filter(f => f.check === "orphan");
|
|
410
|
+
const missingScope = items.filter(f => f.severity === "info");
|
|
411
|
+
const brokenLinks = items.filter(f => f.severity === "warning");
|
|
412
|
+
|
|
413
|
+
lines.push(`Orphans (${items.length}):`);
|
|
414
|
+
if (missingScope.length > 0) {
|
|
415
|
+
lines.push(` - ${missingScope.length} memor${missingScope.length === 1 ? "y" : "ies"} with missing scope`);
|
|
416
|
+
}
|
|
417
|
+
if (brokenLinks.length > 0) {
|
|
418
|
+
for (const f of brokenLinks) {
|
|
419
|
+
lines.push(` - ${f.detail} [${f.memoryIds.map(id => id.slice(0, 8)).join(", ")}]`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
lines.push("");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Health score
|
|
426
|
+
lines.push(`Health Score: ${report.healthScore}/100`);
|
|
427
|
+
|
|
428
|
+
return lines.join("\n");
|
|
429
|
+
}
|
package/src/memory-output.ts
CHANGED
|
@@ -2,6 +2,9 @@ import type { MemoryEntry } from "./store.js";
|
|
|
2
2
|
import type { RetrievalResult } from "./retriever.js";
|
|
3
3
|
import type { RetrievalProfileName } from "./retrieval-profiles.js";
|
|
4
4
|
import { extractMemoryProvenance } from "./memory-boundaries.js";
|
|
5
|
+
import { extractMultiVectorText } from "./multi-vector.js";
|
|
6
|
+
import { parseNarrative } from "./narrative-schema.js";
|
|
7
|
+
import { getConfidence, getConfidenceMetadata } from "./confidence-tracker.js";
|
|
5
8
|
|
|
6
9
|
interface MemoryMetadata {
|
|
7
10
|
source?: string;
|
|
@@ -167,6 +170,7 @@ function getRetrievalPath(result: RetrievalResult): string {
|
|
|
167
170
|
if (result.sources.vector) parts.push("vector");
|
|
168
171
|
if (result.sources.bm25) parts.push("bm25");
|
|
169
172
|
if (result.sources.reranked) parts.push("reranked");
|
|
173
|
+
if (result.sources.narrativeSibling) parts.push("narrative");
|
|
170
174
|
return parts.join("+") || "direct";
|
|
171
175
|
}
|
|
172
176
|
|
|
@@ -289,6 +293,71 @@ function buildSearchRow(index: number, query: string, result: RetrievalResult):
|
|
|
289
293
|
];
|
|
290
294
|
}
|
|
291
295
|
|
|
296
|
+
function extractBriefExcerpt(result: RetrievalResult): string {
|
|
297
|
+
const { l0 } = extractMultiVectorText(result.entry.metadata);
|
|
298
|
+
const raw = l0 || result.entry.text;
|
|
299
|
+
return cleanSnippet(raw, 80);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function formatBriefResults(
|
|
303
|
+
results: RetrievalResult[],
|
|
304
|
+
context: { query: string },
|
|
305
|
+
): string {
|
|
306
|
+
if (results.length === 0) return "No results found.";
|
|
307
|
+
const lines = [`Query: ${context.query}`, `Hits: ${results.length}`, ""];
|
|
308
|
+
for (let i = 0; i < results.length; i++) {
|
|
309
|
+
const r = results[i];
|
|
310
|
+
const excerpt = extractBriefExcerpt(r);
|
|
311
|
+
lines.push(`#${i + 1} ${r.entry.id.slice(0, 8)} ${Math.round(r.score * 100)}% — ${excerpt}`);
|
|
312
|
+
}
|
|
313
|
+
return lines.join("\n");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function formatFullResults(
|
|
317
|
+
results: RetrievalResult[],
|
|
318
|
+
context: RenderContext,
|
|
319
|
+
): string {
|
|
320
|
+
if (results.length === 0) return "No results found.";
|
|
321
|
+
|
|
322
|
+
const lines = [
|
|
323
|
+
`Query : ${context.query}`,
|
|
324
|
+
`Profile : ${context.profile}`,
|
|
325
|
+
`Hits : ${results.length}`,
|
|
326
|
+
"",
|
|
327
|
+
"# ID Score Category Tier Source Date Retrieval Path File / Snippet",
|
|
328
|
+
"-- -------- ----- ------------ ---------- ------- ---------- -------------------- --------------",
|
|
329
|
+
];
|
|
330
|
+
|
|
331
|
+
for (let i = 0; i < results.length; i++) {
|
|
332
|
+
const row = buildSearchRow(i, context.query, results[i]);
|
|
333
|
+
lines.push(`${row[0]} ${row[1]} ${row[2]} ${row[3]} ${row[4]} ${row[5]} ${row[6]} ${row[7]} ${row[8]} | ${row[9]}`);
|
|
334
|
+
lines.push(` prov : ${getProvenanceSummary(results[i])}`);
|
|
335
|
+
// Full mode: append metadata details
|
|
336
|
+
const meta = parseMetadata(results[i].entry);
|
|
337
|
+
const evolution = typeof meta.evolutionStatus === "string" ? meta.evolutionStatus : "-";
|
|
338
|
+
const accessCount = typeof meta.accessCount === "number" ? String(meta.accessCount) : "-";
|
|
339
|
+
const importance = results[i].entry.importance.toFixed(2);
|
|
340
|
+
const tags = Array.isArray(meta.tags) ? (meta.tags as string[]).join(", ") : "-";
|
|
341
|
+
// Emotion metadata (from emotion-detector)
|
|
342
|
+
const emotionPart = meta.emotion && typeof meta.emotion === "object"
|
|
343
|
+
? ` emotion=${(meta.emotion as Record<string, unknown>).label ?? "-"}(v=${(meta.emotion as Record<string, unknown>).valence ?? 0},a=${(meta.emotion as Record<string, unknown>).arousal ?? 0})`
|
|
344
|
+
: "";
|
|
345
|
+
// HP-narrative: Narrative metadata (from narrative-tagger)
|
|
346
|
+
const narrative = parseNarrative(results[i].entry.metadata);
|
|
347
|
+
const narrativePart = narrative
|
|
348
|
+
? ` narrative=${narrative.lifePeriodLabel}/${narrative.generalEventLabel}`
|
|
349
|
+
: "";
|
|
350
|
+
// F1: Confidence metadata
|
|
351
|
+
const confMeta = getConfidenceMetadata(results[i].entry);
|
|
352
|
+
const confPart = confMeta
|
|
353
|
+
? ` confidence=${confMeta.score.toFixed(2)}(${confMeta.reliability})`
|
|
354
|
+
: "";
|
|
355
|
+
lines.push(` meta : evolution=${evolution} accessCount=${accessCount} importance=${importance} tags=[${tags}]${confPart}${emotionPart}${narrativePart}`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return lines.join("\n");
|
|
359
|
+
}
|
|
360
|
+
|
|
292
361
|
export function formatSearchResults(
|
|
293
362
|
results: RetrievalResult[],
|
|
294
363
|
context: RenderContext,
|
package/src/memory-schema.ts
CHANGED
|
@@ -24,18 +24,80 @@ export const ALL_MEMORY_CATEGORIES = [
|
|
|
24
24
|
...LEGACY_MEMORY_CATEGORIES,
|
|
25
25
|
] as const;
|
|
26
26
|
|
|
27
|
-
export const STORE_MEMORY_SOURCES = ["manual", "agent", "api"] as const;
|
|
27
|
+
export const STORE_MEMORY_SOURCES = ["manual", "agent", "api", "session_distill", "conversation_import"] as const;
|
|
28
28
|
|
|
29
29
|
export const DurableMemoryCategorySchema = z.enum(DURABLE_MEMORY_CATEGORIES);
|
|
30
30
|
export const MemoryCategorySchema = z.enum(ALL_MEMORY_CATEGORIES);
|
|
31
31
|
export const StoreMemorySourceSchema = z.enum(STORE_MEMORY_SOURCES);
|
|
32
|
-
export const WriteDispositionSchema = z.enum(["stored", "updated", "deduped", "promoted", "conflict"]);
|
|
32
|
+
export const WriteDispositionSchema = z.enum(["stored", "updated", "deduped", "promoted", "conflict", "rejected"]);
|
|
33
33
|
|
|
34
34
|
export type DurableMemoryCategory = z.infer<typeof DurableMemoryCategorySchema>;
|
|
35
35
|
export type MemoryCategoryValue = z.infer<typeof MemoryCategorySchema>;
|
|
36
36
|
export type StoreMemorySource = z.infer<typeof StoreMemorySourceSchema>;
|
|
37
37
|
export type WriteDisposition = z.infer<typeof WriteDispositionSchema>;
|
|
38
38
|
|
|
39
|
+
// --- Emotional Valence (Philosophy of Memory: Affective Memory) ---
|
|
40
|
+
export interface EmotionMetadata {
|
|
41
|
+
/** Negative (-1) to Positive (+1) */
|
|
42
|
+
valence: number;
|
|
43
|
+
/** Calm (0) to Excited (1) */
|
|
44
|
+
arousal: number;
|
|
45
|
+
/** Human-readable label */
|
|
46
|
+
label?: string;
|
|
47
|
+
/** Composite mnemonic significance: (|valence| + arousal) / 2, range 0-1 */
|
|
48
|
+
salience?: number;
|
|
49
|
+
/** Detection method */
|
|
50
|
+
source?: "keyword" | "llm" | "user";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const EmotionMetadataSchema = z.object({
|
|
54
|
+
valence: z.number().min(-1).max(1),
|
|
55
|
+
arousal: z.number().min(0).max(1),
|
|
56
|
+
label: z.string().max(30).optional(),
|
|
57
|
+
salience: z.number().min(0).max(1).optional(),
|
|
58
|
+
source: z.enum(["keyword", "llm", "user"]).optional(),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
/** Parse emotion from metadata JSON string, returns null if absent */
|
|
62
|
+
export function parseEmotion(metadata: string | undefined): EmotionMetadata | null {
|
|
63
|
+
if (!metadata) return null;
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(metadata);
|
|
66
|
+
if (parsed.emotion && typeof parsed.emotion.valence === "number") {
|
|
67
|
+
return EmotionMetadataSchema.parse(parsed.emotion);
|
|
68
|
+
}
|
|
69
|
+
} catch { /* malformed metadata - safe to ignore */ }
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Default neutral emotion for memories without emotion data */
|
|
74
|
+
export const NEUTRAL_EMOTION: EmotionMetadata = { valence: 0, arousal: 0, label: "neutral" };
|
|
75
|
+
|
|
76
|
+
// --- Privacy Tier (Philosophy of Memory: Ethics Layer) ---
|
|
77
|
+
/**
|
|
78
|
+
* Privacy tiers control how aggressively a memory is persisted, extracted, and shared.
|
|
79
|
+
*
|
|
80
|
+
* - ephemeral: auto-expire, no KG extraction, no sharing — session scratch
|
|
81
|
+
* - private: persist but no KG extraction, no sharing — personal notes
|
|
82
|
+
* - durable: full lifecycle, KG extraction, standard decay — default
|
|
83
|
+
* - shared: marked safe for cross-scope retrieval — collaborative knowledge
|
|
84
|
+
*/
|
|
85
|
+
export const PRIVACY_TIERS = ["ephemeral", "private", "durable", "shared"] as const;
|
|
86
|
+
export type PrivacyTier = (typeof PRIVACY_TIERS)[number];
|
|
87
|
+
export const PrivacyTierSchema = z.enum(PRIVACY_TIERS);
|
|
88
|
+
|
|
89
|
+
/** Parse privacyTier from metadata JSON string, returns "durable" (default) if absent */
|
|
90
|
+
export function parsePrivacyTier(metadata: string | undefined): PrivacyTier {
|
|
91
|
+
if (!metadata) return "durable";
|
|
92
|
+
try {
|
|
93
|
+
const parsed = JSON.parse(metadata);
|
|
94
|
+
if (parsed.privacyTier && PRIVACY_TIERS.includes(parsed.privacyTier)) {
|
|
95
|
+
return parsed.privacyTier;
|
|
96
|
+
}
|
|
97
|
+
} catch { /* malformed metadata - safe default */ }
|
|
98
|
+
return "durable";
|
|
99
|
+
}
|
|
100
|
+
|
|
39
101
|
export const MemoryTextSchema = boundedStringSchema("text", 4000);
|
|
40
102
|
export const MemoryScopeSchema = optionalBoundedStringSchema(160);
|
|
41
103
|
export const RequiredMemoryScopeSchema = boundedStringSchema("scope", 160);
|
|
@@ -64,6 +126,8 @@ export const StoreMemoryInputSchema = z.object({
|
|
|
64
126
|
source: StoreMemorySourceSchema.default("manual"),
|
|
65
127
|
tags: MemoryTagsSchema,
|
|
66
128
|
canonicalKey: CanonicalKeySchema,
|
|
129
|
+
topicTag: z.string().min(1).max(60).optional(),
|
|
130
|
+
privacyTier: PrivacyTierSchema.default("durable"),
|
|
67
131
|
});
|
|
68
132
|
|
|
69
133
|
export const StoredMemoryRecordSchema = StoreMemoryInputSchema.extend({
|
|
@@ -182,3 +246,11 @@ export type CaseMemoryInput = z.infer<typeof CaseMemoryInputSchema>;
|
|
|
182
246
|
export type StoredCaseMemoryRecord = z.infer<typeof StoredCaseMemoryRecordSchema>;
|
|
183
247
|
export type PromoteMemoryInput = z.infer<typeof PromoteMemoryInputSchema>;
|
|
184
248
|
export type StoredPromotedMemoryRecord = z.infer<typeof StoredPromotedMemoryRecordSchema>;
|
|
249
|
+
|
|
250
|
+
export function isEmotionScoringEnabled(): boolean {
|
|
251
|
+
return process.env.RECALLNEST_EMOTION_SCORING === "true";
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function isPredictiveMemoryEnabled(): boolean {
|
|
255
|
+
return process.env.RECALLNEST_PREDICTIVE_MEMORY === "true";
|
|
256
|
+
}
|