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,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forget Engine — Ethics-aware memory deletion with full cascade.
|
|
3
|
+
*
|
|
4
|
+
* Philosophy: "The right to forget" — memories can be explicitly removed,
|
|
5
|
+
* but the process must be auditable, reversible (via evidence export),
|
|
6
|
+
* and propagated to all derived artifacts (KG triples, pins, evolution chains).
|
|
7
|
+
*
|
|
8
|
+
* Sequence:
|
|
9
|
+
* 1. Fetch target memory
|
|
10
|
+
* 2. Privacy tier check (durable requires explicit confirm)
|
|
11
|
+
* 3. Evidence export (snapshot before deletion for audit trail)
|
|
12
|
+
* 4. KG triple cleanup (via KGStore.deleteBySource)
|
|
13
|
+
* 5. Pin archive (mark related pins as forgotten)
|
|
14
|
+
* 6. Cascade demote (related memories via cascade-forget.ts)
|
|
15
|
+
* 7. Primary delete (remove from LanceDB)
|
|
16
|
+
* 8. Audit log (record the forget operation)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { MemoryStore, MemoryEntry } from "./store.js";
|
|
20
|
+
import type { KGStore } from "./kg-store.js";
|
|
21
|
+
import type { AuditLogger } from "./audit-log.js";
|
|
22
|
+
import { parsePrivacyTier, type PrivacyTier } from "./memory-schema.js";
|
|
23
|
+
import { parseEvolution, patchEvolution } from "./memory-evolution.js";
|
|
24
|
+
import { cascadeForget, type CascadeForgetConfig, DEFAULT_CASCADE_FORGET_CONFIG } from "./cascade-forget.js";
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Types
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
export interface ForgetRequest {
|
|
31
|
+
/** Memory ID to forget (full UUID or 8+ hex prefix) */
|
|
32
|
+
memoryId: string;
|
|
33
|
+
/** Explicit confirmation — required for "durable" tier memories */
|
|
34
|
+
confirm: boolean;
|
|
35
|
+
/** Reason for forgetting (audit trail) */
|
|
36
|
+
reason?: string;
|
|
37
|
+
/** Scope filter for permission check */
|
|
38
|
+
scopeFilter?: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ForgetEvidence {
|
|
42
|
+
/** Snapshot of the memory before deletion */
|
|
43
|
+
entry: MemoryEntry;
|
|
44
|
+
/** Privacy tier at time of deletion */
|
|
45
|
+
privacyTier: PrivacyTier;
|
|
46
|
+
/** Evolution chain snapshot */
|
|
47
|
+
evolution: ReturnType<typeof parseEvolution>;
|
|
48
|
+
/** Timestamp of forget operation */
|
|
49
|
+
forgottenAt: string;
|
|
50
|
+
/** Reason provided */
|
|
51
|
+
reason?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ForgetResult {
|
|
55
|
+
/** Whether the memory was successfully forgotten */
|
|
56
|
+
success: boolean;
|
|
57
|
+
/** ID of the forgotten memory */
|
|
58
|
+
memoryId: string;
|
|
59
|
+
/** Evidence snapshot (for audit/undo) */
|
|
60
|
+
evidence?: ForgetEvidence;
|
|
61
|
+
/** Number of KG triples removed */
|
|
62
|
+
kgTriplesRemoved: boolean;
|
|
63
|
+
/** Cascade demote results */
|
|
64
|
+
cascadeResult: { demotedCount: number; demotedIds: string[] };
|
|
65
|
+
/** Error message if failed */
|
|
66
|
+
error?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ForgetByIdDeps {
|
|
70
|
+
store: MemoryStore;
|
|
71
|
+
kgStore?: KGStore | null;
|
|
72
|
+
auditLogger?: AuditLogger | null;
|
|
73
|
+
cascadeConfig?: CascadeForgetConfig;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Core: Forget a single memory
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
export async function forgetMemory(
|
|
81
|
+
deps: ForgetByIdDeps,
|
|
82
|
+
request: ForgetRequest,
|
|
83
|
+
): Promise<ForgetResult> {
|
|
84
|
+
const { store, kgStore, auditLogger, cascadeConfig } = deps;
|
|
85
|
+
const { memoryId, confirm, reason, scopeFilter } = request;
|
|
86
|
+
|
|
87
|
+
// 1. Fetch target
|
|
88
|
+
const entry = await store.get(memoryId, scopeFilter);
|
|
89
|
+
if (!entry) {
|
|
90
|
+
return {
|
|
91
|
+
success: false,
|
|
92
|
+
memoryId,
|
|
93
|
+
kgTriplesRemoved: false,
|
|
94
|
+
cascadeResult: { demotedCount: 0, demotedIds: [] },
|
|
95
|
+
error: `Memory ${memoryId} not found`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 2. Privacy tier check
|
|
100
|
+
const privacyTier = parsePrivacyTier(entry.metadata);
|
|
101
|
+
if (privacyTier === "durable" && !confirm) {
|
|
102
|
+
return {
|
|
103
|
+
success: false,
|
|
104
|
+
memoryId: entry.id,
|
|
105
|
+
kgTriplesRemoved: false,
|
|
106
|
+
cascadeResult: { demotedCount: 0, demotedIds: [] },
|
|
107
|
+
error: `Memory ${entry.id} has privacy tier "durable" — set confirm=true to proceed`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 3. Evidence export (snapshot before deletion)
|
|
112
|
+
const evolution = parseEvolution(entry.metadata, entry.timestamp);
|
|
113
|
+
const evidence: ForgetEvidence = {
|
|
114
|
+
entry: { ...entry },
|
|
115
|
+
privacyTier,
|
|
116
|
+
evolution,
|
|
117
|
+
forgottenAt: new Date().toISOString(),
|
|
118
|
+
reason,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// 4. KG triple cleanup
|
|
122
|
+
let kgTriplesRemoved = false;
|
|
123
|
+
if (kgStore) {
|
|
124
|
+
try {
|
|
125
|
+
await kgStore.deleteBySource(entry.id);
|
|
126
|
+
kgTriplesRemoved = true;
|
|
127
|
+
} catch (err) {
|
|
128
|
+
console.error("[recallnest] KG cleanup failed during forget:", err instanceof Error ? err.message : String(err));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 5. Cascade demote (related memories get importance reduction)
|
|
133
|
+
let cascadeResult = { demotedCount: 0, demotedIds: [] as string[] };
|
|
134
|
+
try {
|
|
135
|
+
cascadeResult = await cascadeForget(
|
|
136
|
+
store,
|
|
137
|
+
{ id: entry.id, vector: entry.vector, scope: entry.scope },
|
|
138
|
+
cascadeConfig ?? DEFAULT_CASCADE_FORGET_CONFIG,
|
|
139
|
+
);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
console.error("[recallnest] Cascade demote failed during forget:", err instanceof Error ? err.message : String(err));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 6. Mark evolution status as "forgotten" before delete (audit breadcrumb)
|
|
145
|
+
try {
|
|
146
|
+
const patchedMetadata = patchEvolution(entry.metadata, {
|
|
147
|
+
status: "archived" as any,
|
|
148
|
+
evolutionNote: `forgotten: ${reason || "user request"}`,
|
|
149
|
+
});
|
|
150
|
+
await store.update(entry.id, { metadata: patchedMetadata }, scopeFilter);
|
|
151
|
+
} catch (err) {
|
|
152
|
+
console.error("[recallnest] Evolution patch failed during forget:", err instanceof Error ? err.message : String(err));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 7. Primary delete
|
|
156
|
+
try {
|
|
157
|
+
await store.delete(entry.id, scopeFilter);
|
|
158
|
+
} catch (err) {
|
|
159
|
+
return {
|
|
160
|
+
success: false,
|
|
161
|
+
memoryId: entry.id,
|
|
162
|
+
evidence,
|
|
163
|
+
kgTriplesRemoved,
|
|
164
|
+
cascadeResult,
|
|
165
|
+
error: `Delete failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 8. Audit log
|
|
170
|
+
try {
|
|
171
|
+
auditLogger?.log({
|
|
172
|
+
operation: "forget",
|
|
173
|
+
scope: entry.scope,
|
|
174
|
+
memoryId: entry.id,
|
|
175
|
+
actor: "system",
|
|
176
|
+
details: `tier=${privacyTier} reason=${reason || "none"} cascade=${cascadeResult.demotedCount}`,
|
|
177
|
+
});
|
|
178
|
+
if (cascadeResult.demotedCount > 0) {
|
|
179
|
+
auditLogger?.log({
|
|
180
|
+
operation: "cascade_forget",
|
|
181
|
+
scope: entry.scope,
|
|
182
|
+
memoryId: entry.id,
|
|
183
|
+
actor: "system",
|
|
184
|
+
details: `demoted ${cascadeResult.demotedCount} related memories: ${cascadeResult.demotedIds.map(id => id.slice(0, 8)).join(",")}`,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
} catch (err) {
|
|
188
|
+
console.error("[recallnest] Audit log failed during forget:", err instanceof Error ? err.message : String(err));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
success: true,
|
|
193
|
+
memoryId: entry.id,
|
|
194
|
+
evidence,
|
|
195
|
+
kgTriplesRemoved,
|
|
196
|
+
cascadeResult,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// Bulk: Forget all memories in a scope
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
export interface ForgetByScopeResult {
|
|
205
|
+
forgottenCount: number;
|
|
206
|
+
failedCount: number;
|
|
207
|
+
kgScopeCleared: boolean;
|
|
208
|
+
totalCascadeDemoted: number;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export async function forgetByScope(
|
|
212
|
+
deps: ForgetByIdDeps,
|
|
213
|
+
scope: string,
|
|
214
|
+
confirm: boolean,
|
|
215
|
+
reason?: string,
|
|
216
|
+
): Promise<ForgetByScopeResult> {
|
|
217
|
+
if (!confirm) {
|
|
218
|
+
return { forgottenCount: 0, failedCount: 0, kgScopeCleared: false, totalCascadeDemoted: 0 };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const { store, kgStore, auditLogger } = deps;
|
|
222
|
+
let forgottenCount = 0;
|
|
223
|
+
let failedCount = 0;
|
|
224
|
+
let totalCascadeDemoted = 0;
|
|
225
|
+
|
|
226
|
+
// Fetch all entries in scope
|
|
227
|
+
const entries = await store.list([scope], undefined, 5000);
|
|
228
|
+
|
|
229
|
+
for (const entry of entries) {
|
|
230
|
+
const result = await forgetMemory(deps, {
|
|
231
|
+
memoryId: entry.id,
|
|
232
|
+
confirm: true,
|
|
233
|
+
reason: reason || `scope-level forget: ${scope}`,
|
|
234
|
+
scopeFilter: [scope],
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
if (result.success) {
|
|
238
|
+
forgottenCount++;
|
|
239
|
+
totalCascadeDemoted += result.cascadeResult.demotedCount;
|
|
240
|
+
} else {
|
|
241
|
+
failedCount++;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Bulk KG scope cleanup
|
|
246
|
+
let kgScopeCleared = false;
|
|
247
|
+
if (kgStore) {
|
|
248
|
+
try {
|
|
249
|
+
await kgStore.deleteByScope(scope);
|
|
250
|
+
kgScopeCleared = true;
|
|
251
|
+
} catch (err) {
|
|
252
|
+
console.error("[recallnest] KG scope cleanup failed:", err instanceof Error ? err.message : String(err));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Audit the bulk operation
|
|
257
|
+
try {
|
|
258
|
+
auditLogger?.log({
|
|
259
|
+
operation: "forget",
|
|
260
|
+
scope,
|
|
261
|
+
actor: "system",
|
|
262
|
+
details: `scope-forget: ${forgottenCount} deleted, ${failedCount} failed, ${totalCascadeDemoted} cascade-demoted`,
|
|
263
|
+
});
|
|
264
|
+
} catch (err) {
|
|
265
|
+
console.error("[recallnest] Audit log failed during scope forget:", err instanceof Error ? err.message : String(err));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return { forgottenCount, failedCount, kgScopeCleared, totalCascadeDemoted };
|
|
269
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P0.2 — Frequency Tracker
|
|
3
|
+
*
|
|
4
|
+
* Tracks how often each memory_id gets retrieved (hit_count).
|
|
5
|
+
* Used to boost frequently accessed memories in scoring.
|
|
6
|
+
*
|
|
7
|
+
* Key design decisions:
|
|
8
|
+
* - Counts per memory_id, not per query text (avoids synonym fragmentation)
|
|
9
|
+
* - Persists to JSON file, loaded on startup
|
|
10
|
+
* - Time decay: 30 days without a hit → effective count halves
|
|
11
|
+
* - Superseded entries do NOT inherit hit_count (fresh starts)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
15
|
+
import { dirname, join } from "path";
|
|
16
|
+
import { logWarn } from "./stderr-log.js";
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Types
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
export interface FrequencyEntry {
|
|
23
|
+
hitCount: number;
|
|
24
|
+
lastHitAt: number; // ms since epoch
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface FrequencyStats {
|
|
28
|
+
[memoryId: string]: FrequencyEntry;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface FrequencyTrackerConfig {
|
|
32
|
+
/** Path to persistence file. Default: data/frequency-stats.json */
|
|
33
|
+
filePath: string;
|
|
34
|
+
/** Boost factor per log2(hitCount). Default: 0.15 */
|
|
35
|
+
boostFactor: number;
|
|
36
|
+
/** Days without a hit before effective count halves. Default: 30 */
|
|
37
|
+
decayHalfLifeDays: number;
|
|
38
|
+
/** Minimum hits before boost kicks in. Default: 2 */
|
|
39
|
+
minHitsForBoost: number;
|
|
40
|
+
/** Hit count threshold for auto-promoting to core tier. Default: 3 */
|
|
41
|
+
corePromotionThreshold: number;
|
|
42
|
+
/** Debounce write interval (ms). Default: 10000 */
|
|
43
|
+
flushIntervalMs: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const DEFAULT_FREQUENCY_CONFIG: FrequencyTrackerConfig = {
|
|
47
|
+
filePath: join(process.cwd(), "data", "frequency-stats.json"),
|
|
48
|
+
boostFactor: 0.15,
|
|
49
|
+
decayHalfLifeDays: 30,
|
|
50
|
+
minHitsForBoost: 2,
|
|
51
|
+
corePromotionThreshold: 3,
|
|
52
|
+
flushIntervalMs: 10000,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// Frequency Tracker
|
|
57
|
+
// ============================================================================
|
|
58
|
+
|
|
59
|
+
export class FrequencyTracker {
|
|
60
|
+
private stats: FrequencyStats = {};
|
|
61
|
+
private dirty = false;
|
|
62
|
+
private flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
63
|
+
readonly config: FrequencyTrackerConfig;
|
|
64
|
+
|
|
65
|
+
constructor(config?: Partial<FrequencyTrackerConfig>) {
|
|
66
|
+
this.config = { ...DEFAULT_FREQUENCY_CONFIG, ...config };
|
|
67
|
+
this.load();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Record hits for a set of memory IDs (called after retrieval). */
|
|
71
|
+
recordHits(memoryIds: string[]): void {
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
for (const id of memoryIds) {
|
|
74
|
+
const existing = this.stats[id];
|
|
75
|
+
if (existing) {
|
|
76
|
+
existing.hitCount += 1;
|
|
77
|
+
existing.lastHitAt = now;
|
|
78
|
+
} else {
|
|
79
|
+
this.stats[id] = { hitCount: 1, lastHitAt: now };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
this.dirty = true;
|
|
83
|
+
this.scheduleSave();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Compute the frequency boost multiplier for a memory.
|
|
88
|
+
* Returns 1.0 (no boost) if below threshold.
|
|
89
|
+
* Formula: 1 + log2(effectiveHits) * boostFactor
|
|
90
|
+
*/
|
|
91
|
+
getBoostMultiplier(memoryId: string): number {
|
|
92
|
+
const entry = this.stats[memoryId];
|
|
93
|
+
if (!entry || entry.hitCount < this.config.minHitsForBoost) return 1.0;
|
|
94
|
+
|
|
95
|
+
const effectiveHits = this.effectiveHitCount(entry);
|
|
96
|
+
if (effectiveHits < this.config.minHitsForBoost) return 1.0;
|
|
97
|
+
|
|
98
|
+
return 1 + Math.log2(effectiveHits) * this.config.boostFactor;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Check if a memory should be auto-promoted to core tier. */
|
|
102
|
+
shouldPromoteToCore(memoryId: string): boolean {
|
|
103
|
+
const entry = this.stats[memoryId];
|
|
104
|
+
if (!entry) return false;
|
|
105
|
+
return this.effectiveHitCount(entry) >= this.config.corePromotionThreshold;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Get raw stats for a memory (for debugging/testing). */
|
|
109
|
+
getStats(memoryId: string): FrequencyEntry | undefined {
|
|
110
|
+
return this.stats[memoryId];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Total tracked entries. */
|
|
114
|
+
get size(): number {
|
|
115
|
+
return Object.keys(this.stats).length;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Force flush to disk (for testing or shutdown). */
|
|
119
|
+
flush(): void {
|
|
120
|
+
if (!this.dirty) return;
|
|
121
|
+
this.save();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Clean up timer on shutdown. */
|
|
125
|
+
dispose(): void {
|
|
126
|
+
if (this.flushTimer) {
|
|
127
|
+
clearTimeout(this.flushTimer);
|
|
128
|
+
this.flushTimer = null;
|
|
129
|
+
}
|
|
130
|
+
this.flush();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// --------------------------------------------------------------------------
|
|
134
|
+
// Internal
|
|
135
|
+
// --------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
private effectiveHitCount(entry: FrequencyEntry): number {
|
|
138
|
+
const daysSinceHit = (Date.now() - entry.lastHitAt) / 86_400_000;
|
|
139
|
+
const decayFactor = Math.exp(
|
|
140
|
+
(-daysSinceHit * Math.LN2) / this.config.decayHalfLifeDays,
|
|
141
|
+
);
|
|
142
|
+
return entry.hitCount * decayFactor;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private load(): void {
|
|
146
|
+
try {
|
|
147
|
+
if (existsSync(this.config.filePath)) {
|
|
148
|
+
const raw = readFileSync(this.config.filePath, "utf-8");
|
|
149
|
+
this.stats = JSON.parse(raw) as FrequencyStats;
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
logWarn("frequency-tracker: failed to load stats, starting fresh");
|
|
153
|
+
this.stats = {};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private save(): void {
|
|
158
|
+
try {
|
|
159
|
+
const dir = dirname(this.config.filePath);
|
|
160
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
161
|
+
writeFileSync(this.config.filePath, JSON.stringify(this.stats, null, 2));
|
|
162
|
+
this.dirty = false;
|
|
163
|
+
} catch (err) {
|
|
164
|
+
logWarn(`frequency-tracker: failed to save stats: ${err}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private scheduleSave(): void {
|
|
169
|
+
if (this.flushTimer) return;
|
|
170
|
+
this.flushTimer = setTimeout(() => {
|
|
171
|
+
this.flushTimer = null;
|
|
172
|
+
this.save();
|
|
173
|
+
}, this.config.flushIntervalMs);
|
|
174
|
+
}
|
|
175
|
+
}
|