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
package/src/capture-engine.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import type { Embedder } from "./embedder.js";
|
|
2
|
+
import { detectLang, tokenizeFts } from "./language-hook.js";
|
|
3
|
+
import { generateAnchor } from "./anchor-generator.js";
|
|
4
|
+
import { incrementWriteCount } from "./activity-counter.js";
|
|
5
|
+
import { verifyWrite } from "./write-verifier.js";
|
|
6
|
+
// batchInternalDedup is available in ingest.ts for large-batch scenarios;
|
|
7
|
+
// persistMemoryBatch relies on per-item conflict detection (A-2) instead.
|
|
8
|
+
import { defaultEvolution, buildSupersedeMetadata, buildSupersedeMetadataForNew, buildPendingReviewMetadata, isActiveMemory, patchEvolution } from "./memory-evolution.js";
|
|
9
|
+
import { detectTopicTag, injectTopicTag } from "./topic-tag.js";
|
|
10
|
+
import { assignDefaultConfidence, type ConfidenceMetadata } from "./confidence-tracker.js";
|
|
2
11
|
import {
|
|
3
12
|
type CaseMemoryInput,
|
|
4
13
|
CaseMemoryInputSchema,
|
|
@@ -17,6 +26,7 @@ import {
|
|
|
17
26
|
type DurableMemoryCategory,
|
|
18
27
|
DURABLE_MEMORY_CATEGORIES,
|
|
19
28
|
type WriteDisposition,
|
|
29
|
+
parsePrivacyTier,
|
|
20
30
|
} from "./memory-schema.js";
|
|
21
31
|
import { deterministicId, type MemoryEntry, type MemoryStore } from "./store.js";
|
|
22
32
|
import {
|
|
@@ -36,6 +46,7 @@ import { buildConflictCandidateRecord, buildConflictFingerprint, reopenConflictC
|
|
|
36
46
|
import type { ConflictCandidateStore } from "./conflict-store.js";
|
|
37
47
|
import {
|
|
38
48
|
inferPreferenceSlot,
|
|
49
|
+
inferImplicitUsageSlot,
|
|
39
50
|
samePreferenceSlot,
|
|
40
51
|
type AtomicBrandItemPreferenceSlot,
|
|
41
52
|
type PreferenceSlot,
|
|
@@ -45,11 +56,15 @@ import {
|
|
|
45
56
|
import { matchPreference, applyPreferenceMatch } from "./preference-matcher.js";
|
|
46
57
|
import type { LLMClient } from "./llm-client.js";
|
|
47
58
|
import type { KGExtractor } from "./kg-extractor.js";
|
|
59
|
+
import { scanForPII } from "./pii-detector.js";
|
|
60
|
+
import type { AuditLogger } from "./audit-log.js";
|
|
61
|
+
import { checkAdmission, type ScopeRateLimiter, type AdmissionConfig } from "./admission-control.js";
|
|
62
|
+
import { tagNarrativeIfEnabled } from "./narrative-tagger.js";
|
|
48
63
|
|
|
49
64
|
type StoreDeps = Pick<MemoryStore, "store"> & Partial<Pick<MemoryStore, "list" | "update" | "getById" | "get" | "vectorSearch">>;
|
|
50
65
|
type ConflictStoreDeps = Pick<ConflictCandidateStore, "save" | "replace" | "getOpenByFingerprint" | "getLatestByFingerprint">;
|
|
51
66
|
|
|
52
|
-
interface PersistMemoryDeps {
|
|
67
|
+
export interface PersistMemoryDeps {
|
|
53
68
|
store: StoreDeps;
|
|
54
69
|
embedder: Pick<Embedder, "embedPassage">;
|
|
55
70
|
conflictStore?: ConflictStoreDeps;
|
|
@@ -57,6 +72,12 @@ interface PersistMemoryDeps {
|
|
|
57
72
|
llm?: LLMClient | null;
|
|
58
73
|
/** Tier 4.1: Optional KG triple extractor (async, non-blocking) */
|
|
59
74
|
kgExtractor?: KGExtractor | null;
|
|
75
|
+
/** F-1: Optional audit logger for recording store operations */
|
|
76
|
+
auditLogger?: AuditLogger | null;
|
|
77
|
+
/** LME-8: Optional rate limiter for admission control */
|
|
78
|
+
rateLimiter?: ScopeRateLimiter | null;
|
|
79
|
+
/** LME-8: Optional admission config overrides */
|
|
80
|
+
admissionConfig?: Partial<AdmissionConfig>;
|
|
60
81
|
}
|
|
61
82
|
|
|
62
83
|
interface DurableWriteInput {
|
|
@@ -71,6 +92,8 @@ interface DurableWriteInput {
|
|
|
71
92
|
source: "manual" | "agent" | "api";
|
|
72
93
|
sourceCategory?: string;
|
|
73
94
|
sourceBoundary?: ReturnType<typeof extractBoundaryMetadata>;
|
|
95
|
+
language?: string;
|
|
96
|
+
fts_text?: string;
|
|
74
97
|
}
|
|
75
98
|
|
|
76
99
|
const CANONICAL_SCAN_LIMIT = 1000;
|
|
@@ -124,14 +147,28 @@ function buildStructuredMetadata(params: {
|
|
|
124
147
|
category: DurableMemoryCategory;
|
|
125
148
|
canonicalKey: string;
|
|
126
149
|
extra?: Record<string, unknown>;
|
|
150
|
+
/** Optional: scope + text for narrative tagging (Phase 3) */
|
|
151
|
+
narrativeInput?: { scope: string; text: string; sessionId?: string };
|
|
127
152
|
}): string {
|
|
153
|
+
// HP-narrative: Tag with autobiographical narrative metadata when enabled
|
|
154
|
+
const narrative = params.narrativeInput
|
|
155
|
+
? tagNarrativeIfEnabled({
|
|
156
|
+
scope: params.narrativeInput.scope,
|
|
157
|
+
text: params.narrativeInput.text,
|
|
158
|
+
timestamp: Date.now(),
|
|
159
|
+
sessionId: params.narrativeInput.sessionId,
|
|
160
|
+
})
|
|
161
|
+
: null;
|
|
162
|
+
|
|
128
163
|
return JSON.stringify({
|
|
129
164
|
source: params.source,
|
|
130
165
|
tags: params.tags,
|
|
131
166
|
capture: params.capture,
|
|
132
167
|
boundary: buildStructuredMemoryBoundary(params.category),
|
|
133
168
|
canonicalKey: params.canonicalKey,
|
|
169
|
+
evolution: defaultEvolution(),
|
|
134
170
|
...params.extra,
|
|
171
|
+
...(narrative ? { narrative } : {}),
|
|
135
172
|
});
|
|
136
173
|
}
|
|
137
174
|
|
|
@@ -533,6 +570,8 @@ async function writeDurableEntry(
|
|
|
533
570
|
category: params.category,
|
|
534
571
|
metadata: params.metadata,
|
|
535
572
|
timestamp: Date.now(),
|
|
573
|
+
...(params.language ? { language: params.language } : {}),
|
|
574
|
+
...(params.fts_text ? { fts_text: params.fts_text } : {}),
|
|
536
575
|
});
|
|
537
576
|
if (!updated) {
|
|
538
577
|
throw new Error(`Failed to update canonical memory ${latest.id}`);
|
|
@@ -553,6 +592,8 @@ async function writeDurableEntry(
|
|
|
553
592
|
scope: params.scope,
|
|
554
593
|
importance: params.importance,
|
|
555
594
|
metadata: params.metadata,
|
|
595
|
+
...(params.language ? { language: params.language } : {}),
|
|
596
|
+
...(params.fts_text ? { fts_text: params.fts_text } : {}),
|
|
556
597
|
});
|
|
557
598
|
|
|
558
599
|
return {
|
|
@@ -599,13 +640,20 @@ function inferPromotedCategory(
|
|
|
599
640
|
}
|
|
600
641
|
|
|
601
642
|
function buildStoreMemoryMetadata(input: StoreMemoryInput, canonicalKey: string): string {
|
|
643
|
+
const anchor = generateAnchor(input.text);
|
|
644
|
+
const anchorExtra = anchor ? { anchor } : undefined;
|
|
645
|
+
// HP-ethics: Include privacyTier in metadata if non-default
|
|
646
|
+
const privacyExtra = input.privacyTier && input.privacyTier !== "durable"
|
|
647
|
+
? { privacyTier: input.privacyTier }
|
|
648
|
+
: undefined;
|
|
602
649
|
return buildStructuredMetadata({
|
|
603
650
|
source: input.source,
|
|
604
651
|
tags: input.tags,
|
|
605
652
|
capture: "store_memory_schema_v1",
|
|
606
653
|
category: input.category,
|
|
607
654
|
canonicalKey,
|
|
608
|
-
extra: buildPreferenceSlotExtra(input.category, input.text),
|
|
655
|
+
extra: mergeExtra(buildPreferenceSlotExtra(input.category, input.text), anchorExtra, privacyExtra),
|
|
656
|
+
narrativeInput: { scope: input.scope, text: input.text },
|
|
609
657
|
});
|
|
610
658
|
}
|
|
611
659
|
|
|
@@ -614,21 +662,25 @@ function buildWorkflowPatternMetadata(
|
|
|
614
662
|
tags: string[],
|
|
615
663
|
canonicalKey: string,
|
|
616
664
|
): string {
|
|
665
|
+
const wpExtra: Record<string, unknown> = {
|
|
666
|
+
workflowPattern: {
|
|
667
|
+
title: input.title,
|
|
668
|
+
trigger: input.trigger,
|
|
669
|
+
steps: input.steps,
|
|
670
|
+
outcome: input.outcome,
|
|
671
|
+
tools: input.tools,
|
|
672
|
+
},
|
|
673
|
+
};
|
|
674
|
+
const anchor = generateAnchor(input.title + ": " + input.trigger, wpExtra);
|
|
675
|
+
if (anchor) wpExtra.anchor = anchor;
|
|
617
676
|
return buildStructuredMetadata({
|
|
618
677
|
source: input.source,
|
|
619
678
|
tags,
|
|
620
679
|
capture: "workflow_pattern_schema_v1",
|
|
621
680
|
category: "patterns",
|
|
622
681
|
canonicalKey,
|
|
623
|
-
extra:
|
|
624
|
-
|
|
625
|
-
title: input.title,
|
|
626
|
-
trigger: input.trigger,
|
|
627
|
-
steps: input.steps,
|
|
628
|
-
outcome: input.outcome,
|
|
629
|
-
tools: input.tools,
|
|
630
|
-
},
|
|
631
|
-
},
|
|
682
|
+
extra: wpExtra,
|
|
683
|
+
narrativeInput: { scope: input.scope, text: input.title + ": " + input.trigger },
|
|
632
684
|
});
|
|
633
685
|
}
|
|
634
686
|
|
|
@@ -637,22 +689,26 @@ function buildCaseMemoryMetadata(
|
|
|
637
689
|
tags: string[],
|
|
638
690
|
canonicalKey: string,
|
|
639
691
|
): string {
|
|
692
|
+
const cmExtra: Record<string, unknown> = {
|
|
693
|
+
caseMemory: {
|
|
694
|
+
title: input.title,
|
|
695
|
+
problem: input.problem,
|
|
696
|
+
context: input.context,
|
|
697
|
+
solutionSteps: input.solutionSteps,
|
|
698
|
+
outcome: input.outcome,
|
|
699
|
+
tools: input.tools,
|
|
700
|
+
},
|
|
701
|
+
};
|
|
702
|
+
const anchor = generateAnchor(input.title + ": " + input.problem, cmExtra);
|
|
703
|
+
if (anchor) cmExtra.anchor = anchor;
|
|
640
704
|
return buildStructuredMetadata({
|
|
641
705
|
source: input.source,
|
|
642
706
|
tags,
|
|
643
707
|
capture: "case_memory_schema_v1",
|
|
644
708
|
category: "cases",
|
|
645
709
|
canonicalKey,
|
|
646
|
-
extra:
|
|
647
|
-
|
|
648
|
-
title: input.title,
|
|
649
|
-
problem: input.problem,
|
|
650
|
-
context: input.context,
|
|
651
|
-
solutionSteps: input.solutionSteps,
|
|
652
|
-
outcome: input.outcome,
|
|
653
|
-
tools: input.tools,
|
|
654
|
-
},
|
|
655
|
-
},
|
|
710
|
+
extra: cmExtra,
|
|
711
|
+
narrativeInput: { scope: input.scope, text: input.title + ": " + input.problem },
|
|
656
712
|
});
|
|
657
713
|
}
|
|
658
714
|
|
|
@@ -682,6 +738,7 @@ function buildPromotionMetadata(
|
|
|
682
738
|
},
|
|
683
739
|
},
|
|
684
740
|
),
|
|
741
|
+
narrativeInput: { scope: input.scope, text },
|
|
685
742
|
});
|
|
686
743
|
}
|
|
687
744
|
|
|
@@ -690,7 +747,77 @@ export async function persistMemory(
|
|
|
690
747
|
rawInput: unknown,
|
|
691
748
|
): Promise<StoredMemoryRecord> {
|
|
692
749
|
const input = StoreMemoryInputSchema.parse(rawInput);
|
|
750
|
+
|
|
751
|
+
// F3: Extract temporal validity params before Zod strips them
|
|
752
|
+
const rawObj = rawInput as Record<string, unknown> | null;
|
|
753
|
+
const validUntilRaw = rawObj?.validUntil;
|
|
754
|
+
const eventTimeRaw = rawObj?.eventTime;
|
|
755
|
+
|
|
756
|
+
// LC-P5: Large text gate — texts > 8000 chars get truncated to L0 summary.
|
|
757
|
+
// Prevents oversized entries from bloating the index and degrading embedding quality.
|
|
758
|
+
const LARGE_TEXT_THRESHOLD = 8000;
|
|
759
|
+
if (input.text.length > LARGE_TEXT_THRESHOLD && deps.llm) {
|
|
760
|
+
try {
|
|
761
|
+
const summary = await deps.llm.generateCoreSummary(input.text);
|
|
762
|
+
if (summary) {
|
|
763
|
+
// Store original length in metadata for reference, replace text with summary
|
|
764
|
+
const origLength = input.text.length;
|
|
765
|
+
input.text = summary;
|
|
766
|
+
input.tags = [...(input.tags ?? []), `truncated:${origLength}`];
|
|
767
|
+
}
|
|
768
|
+
} catch {
|
|
769
|
+
// Fallback: hard truncate if LLM fails
|
|
770
|
+
input.text = input.text.slice(0, LARGE_TEXT_THRESHOLD);
|
|
771
|
+
}
|
|
772
|
+
} else if (input.text.length > LARGE_TEXT_THRESHOLD) {
|
|
773
|
+
// No LLM available — hard truncate
|
|
774
|
+
input.text = input.text.slice(0, LARGE_TEXT_THRESHOLD);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// B-3: LLM importance assessment — refine default importance (0.7) via LLM.
|
|
778
|
+
// Only fires when importance is at the default value and LLM is available.
|
|
779
|
+
// Explicit importance values from the caller are respected as-is.
|
|
780
|
+
let pendingReview = false; // HP-6: track low-confidence assessment
|
|
781
|
+
if (input.importance === 0.7 && deps.llm) {
|
|
782
|
+
try {
|
|
783
|
+
const assessed = await deps.llm.assessImportance(input.text, input.category);
|
|
784
|
+
if (assessed !== null) {
|
|
785
|
+
input.importance = assessed;
|
|
786
|
+
// HP-6: Very low importance from LLM → pending review for distill to re-evaluate
|
|
787
|
+
if (assessed < 0.3) pendingReview = true;
|
|
788
|
+
}
|
|
789
|
+
} catch {
|
|
790
|
+
// LLM importance assessment must never block memory writes
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// LME-8: Admission control — reject low-quality writes before expensive embedding
|
|
795
|
+
const admissionScope = input.scope || "unknown";
|
|
796
|
+
const admission = checkAdmission(
|
|
797
|
+
input.text,
|
|
798
|
+
input.importance,
|
|
799
|
+
admissionScope,
|
|
800
|
+
deps.rateLimiter ?? undefined,
|
|
801
|
+
deps.admissionConfig,
|
|
802
|
+
);
|
|
803
|
+
if (admission.verdict === "rejected") {
|
|
804
|
+
return {
|
|
805
|
+
id: `rejected-${Date.now()}`,
|
|
806
|
+
text: input.text,
|
|
807
|
+
category: input.category,
|
|
808
|
+
importance: input.importance,
|
|
809
|
+
scope: admissionScope,
|
|
810
|
+
source: input.source,
|
|
811
|
+
tags: input.tags ?? [],
|
|
812
|
+
resolvedScope: admissionScope,
|
|
813
|
+
storedAt: new Date().toISOString(),
|
|
814
|
+
disposition: "rejected" as const,
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
|
|
693
818
|
const vector = await deps.embedder.embedPassage(input.text);
|
|
819
|
+
const language = detectLang(input.text);
|
|
820
|
+
const fts_text = tokenizeFts(input.text, language);
|
|
694
821
|
const resolvedScope = resolveScope(input);
|
|
695
822
|
const canonicalKey = resolveCanonicalKey({
|
|
696
823
|
category: input.category,
|
|
@@ -738,24 +865,229 @@ export async function persistMemory(
|
|
|
738
865
|
}
|
|
739
866
|
}
|
|
740
867
|
|
|
868
|
+
// A-2: Evolution-aware semantic conflict detection.
|
|
869
|
+
// For non-preferences categories, check if a semantically similar memory
|
|
870
|
+
// already exists in the same scope. If so, use LLM to decide whether the
|
|
871
|
+
// old memory should be superseded.
|
|
872
|
+
// Requires both vectorSearch (for candidate lookup) and LLM (for judgment).
|
|
873
|
+
// Graceful fallback: if either is unavailable, skip detection entirely.
|
|
874
|
+
let supersededOldId: string | null = null; // HP-1: track for bidirectional link
|
|
875
|
+
let supersededReason: string | null = null;
|
|
876
|
+
if (
|
|
877
|
+
input.category !== "preferences" &&
|
|
878
|
+
deps.store.vectorSearch &&
|
|
879
|
+
deps.llm &&
|
|
880
|
+
deps.store.update
|
|
881
|
+
) {
|
|
882
|
+
try {
|
|
883
|
+
const candidates = await deps.store.vectorSearch(vector, 3, 0.85, [resolvedScope]);
|
|
884
|
+
// Only consider same-category, active memories as supersede targets
|
|
885
|
+
const supersedeTargets = candidates.filter(
|
|
886
|
+
c => c.entry.category === input.category &&
|
|
887
|
+
c.entry.id !== deterministicId(resolvedScope, input.text) &&
|
|
888
|
+
isActiveMemory(c.entry.metadata),
|
|
889
|
+
);
|
|
890
|
+
if (supersedeTargets.length > 0) {
|
|
891
|
+
const decision = await deps.llm.dedupDecision(input.text, supersedeTargets[0].entry.text);
|
|
892
|
+
if (decision.action === "MERGE" || decision.action === "SKIP") {
|
|
893
|
+
// LLM says new info overlaps with existing → supersede the old one
|
|
894
|
+
// (MERGE = new has extra info → store new, mark old superseded)
|
|
895
|
+
// (SKIP = identical → just dedup, handled by writeDurableEntry)
|
|
896
|
+
if (decision.action === "MERGE") {
|
|
897
|
+
const oldEntry = supersedeTargets[0].entry;
|
|
898
|
+
const supersededMeta = buildSupersedeMetadata(oldEntry.metadata, deterministicId(resolvedScope, input.text));
|
|
899
|
+
await deps.store.update(oldEntry.id, { metadata: supersededMeta });
|
|
900
|
+
// HP-1: Track for bidirectional link
|
|
901
|
+
supersededOldId = oldEntry.id;
|
|
902
|
+
supersededReason = decision.reason;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
// CREATE or error → proceed normally (store new memory as-is)
|
|
906
|
+
}
|
|
907
|
+
} catch {
|
|
908
|
+
// Evolution conflict detection must never block memory writes
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// F2: Interference pre-warning — if scope already has ≥5 high-similarity memories,
|
|
913
|
+
// mark the weakest as pending_review and record an interferenceNote.
|
|
914
|
+
if (deps.store.vectorSearch && deps.store.update) {
|
|
915
|
+
try {
|
|
916
|
+
const nearMatches = await deps.store.vectorSearch(vector, 6, 0.82, [resolvedScope]);
|
|
917
|
+
const activeNear = nearMatches.filter(
|
|
918
|
+
c => c.entry.id !== deterministicId(resolvedScope, input.text) &&
|
|
919
|
+
isActiveMemory(c.entry.metadata),
|
|
920
|
+
);
|
|
921
|
+
if (activeNear.length >= 5) {
|
|
922
|
+
// Find weakest by importance × confidence
|
|
923
|
+
const weakest = activeNear.reduce((min, cur) => {
|
|
924
|
+
const curScore = (cur.entry.importance ?? 0.5) * cur.score;
|
|
925
|
+
const minScore = (min.entry.importance ?? 0.5) * min.score;
|
|
926
|
+
return curScore < minScore ? cur : min;
|
|
927
|
+
});
|
|
928
|
+
const reviewMeta = buildPendingReviewMetadata(weakest.entry.metadata);
|
|
929
|
+
const parsed: Record<string, unknown> = JSON.parse(reviewMeta);
|
|
930
|
+
const evo = parsed.evolution as Record<string, unknown> | undefined;
|
|
931
|
+
if (evo) evo.evolutionNote = `F2: interference cluster detected (${activeNear.length} similar active memories)`;
|
|
932
|
+
await deps.store.update(weakest.entry.id, { metadata: JSON.stringify(parsed) });
|
|
933
|
+
}
|
|
934
|
+
} catch {
|
|
935
|
+
// Interference detection must never block memory writes
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// F-3: PII detection — inject warning into metadata if PII found.
|
|
940
|
+
// Non-blocking: PII does not prevent the write.
|
|
941
|
+
let metadata = buildStoreMemoryMetadata(input, canonicalKey);
|
|
942
|
+
const piiResult = scanForPII(input.text);
|
|
943
|
+
if (piiResult.hasPII) {
|
|
944
|
+
const parsed: Record<string, unknown> = JSON.parse(metadata);
|
|
945
|
+
parsed.piiWarning = {
|
|
946
|
+
summary: piiResult.summary,
|
|
947
|
+
detections: piiResult.detections.length,
|
|
948
|
+
severity: piiResult.detections.some(d => d.severity === "high") ? "high" : "medium",
|
|
949
|
+
};
|
|
950
|
+
metadata = JSON.stringify(parsed);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// MP-1: Topic Tag — auto-detect or use explicit tag from caller
|
|
954
|
+
const topicTag = (input as Record<string, unknown>).topicTag as string | undefined
|
|
955
|
+
?? detectTopicTag(input.text);
|
|
956
|
+
if (topicTag) {
|
|
957
|
+
metadata = injectTopicTag(metadata, topicTag);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// HP-1: Bidirectional supersede link — mark new memory as superseding old
|
|
961
|
+
if (supersededOldId) {
|
|
962
|
+
metadata = buildSupersedeMetadataForNew(metadata, supersededOldId, supersededReason ?? undefined);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// HP-6: Low-confidence entries → pending_review (distill prioritizes these)
|
|
966
|
+
if (pendingReview) {
|
|
967
|
+
metadata = buildPendingReviewMetadata(metadata);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// F3: Temporal validity — inject validUntil and eventTime into evolution metadata
|
|
971
|
+
if (validUntilRaw != null || eventTimeRaw != null) {
|
|
972
|
+
const temporalPatch: Record<string, number> = {};
|
|
973
|
+
if (validUntilRaw != null) {
|
|
974
|
+
temporalPatch.validUntil = typeof validUntilRaw === "number"
|
|
975
|
+
? validUntilRaw
|
|
976
|
+
: new Date(String(validUntilRaw)).getTime();
|
|
977
|
+
}
|
|
978
|
+
if (eventTimeRaw != null) {
|
|
979
|
+
temporalPatch.eventTime = typeof eventTimeRaw === "number"
|
|
980
|
+
? eventTimeRaw
|
|
981
|
+
: new Date(String(eventTimeRaw)).getTime();
|
|
982
|
+
}
|
|
983
|
+
metadata = patchEvolution(metadata, temporalPatch);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// F1: Assign structured confidence based on source (user can override via explicit param)
|
|
987
|
+
const confidenceRaw = rawObj?.confidence as Partial<ConfidenceMetadata> | number | undefined;
|
|
988
|
+
const explicitConfidence = typeof confidenceRaw === "number"
|
|
989
|
+
? { score: confidenceRaw }
|
|
990
|
+
: (confidenceRaw ?? undefined);
|
|
991
|
+
const confidenceMeta = assignDefaultConfidence(input.source, explicitConfidence);
|
|
992
|
+
{
|
|
993
|
+
const parsed: Record<string, unknown> = JSON.parse(metadata);
|
|
994
|
+
parsed.confidence = confidenceMeta;
|
|
995
|
+
metadata = JSON.stringify(parsed);
|
|
996
|
+
}
|
|
997
|
+
|
|
741
998
|
const { entry, disposition, conflictId } = await writeDurableEntry(deps, {
|
|
742
999
|
text: input.text,
|
|
743
1000
|
vector,
|
|
744
1001
|
category: input.category,
|
|
745
1002
|
scope: resolvedScope,
|
|
746
1003
|
importance: input.importance,
|
|
747
|
-
metadata
|
|
1004
|
+
metadata,
|
|
748
1005
|
canonicalKey,
|
|
749
1006
|
source: input.source,
|
|
1007
|
+
language,
|
|
1008
|
+
fts_text,
|
|
750
1009
|
});
|
|
751
1010
|
|
|
752
1011
|
// Tier 4.1: Async KG triple extraction (non-blocking)
|
|
753
|
-
|
|
1012
|
+
// HP-ethics: Skip KG extraction for ephemeral/private memories — they must not leave graph traces
|
|
1013
|
+
const entryPrivacyTier = parsePrivacyTier(metadata);
|
|
1014
|
+
const kgAllowed = entryPrivacyTier !== "ephemeral" && entryPrivacyTier !== "private";
|
|
1015
|
+
if (deps.kgExtractor && disposition !== "deduped" && kgAllowed) {
|
|
754
1016
|
deps.kgExtractor
|
|
755
1017
|
.extractAndStore(input.text, entry.id, resolvedScope)
|
|
756
1018
|
.catch(() => {}); // Silently ignore — KG extraction must never block memory writes
|
|
757
1019
|
}
|
|
758
1020
|
|
|
1021
|
+
// F-1: Audit log — record store operation (non-blocking, silent on failure)
|
|
1022
|
+
try {
|
|
1023
|
+
deps.auditLogger?.log({
|
|
1024
|
+
operation: "store",
|
|
1025
|
+
scope: resolvedScope,
|
|
1026
|
+
memoryId: entry.id,
|
|
1027
|
+
actor: input.source,
|
|
1028
|
+
details: `${input.category}: ${input.text.slice(0, 100)}`,
|
|
1029
|
+
});
|
|
1030
|
+
} catch {
|
|
1031
|
+
// Audit must never block memory writes
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// HP-3: Activity counter — track non-dedup writes for distill trigger
|
|
1035
|
+
if (disposition !== "deduped") {
|
|
1036
|
+
try {
|
|
1037
|
+
incrementWriteCount();
|
|
1038
|
+
} catch {
|
|
1039
|
+
// Activity tracking must never block memory writes
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// HP-2: Post-write verification — async, non-blocking
|
|
1044
|
+
if (disposition !== "deduped" && deps.store.get) {
|
|
1045
|
+
verifyWrite(deps.store as Pick<import("./store.js").MemoryStore, "get">, entry.id)
|
|
1046
|
+
.then(result => {
|
|
1047
|
+
if (!result.ok) {
|
|
1048
|
+
console.error(
|
|
1049
|
+
`[HP-2] Write verification issues for ${entry.id}: ${result.issues.join(", ")} (${result.durationMs}ms)`,
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
})
|
|
1053
|
+
.catch(() => {}); // Must never block
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// LME-1: Implicit preference dual-write — when a non-preference memory
|
|
1057
|
+
// contains an implicit usage signal ("I use X", "I have X", etc.),
|
|
1058
|
+
// store an additional preferences copy so it can be recalled via the
|
|
1059
|
+
// low-threshold preference retrieval path (hardMinScore 0.25 vs 0.35).
|
|
1060
|
+
// Async & non-blocking: the primary write is already done.
|
|
1061
|
+
if (
|
|
1062
|
+
disposition !== "deduped" &&
|
|
1063
|
+
input.category !== "preferences" &&
|
|
1064
|
+
inferImplicitUsageSlot(input.text)
|
|
1065
|
+
) {
|
|
1066
|
+
const prefCanonicalKey = buildDefaultCanonicalKey("preferences", input.text);
|
|
1067
|
+
const prefSlot = inferImplicitUsageSlot(input.text);
|
|
1068
|
+
const prefMetadata = buildStructuredMetadata({
|
|
1069
|
+
source: input.source,
|
|
1070
|
+
tags: [...(input.tags ?? []), "derived-preference", `derived-from:${entry.id}`],
|
|
1071
|
+
capture: "implicit_preference_dual_write_v1",
|
|
1072
|
+
category: "preferences",
|
|
1073
|
+
canonicalKey: prefCanonicalKey,
|
|
1074
|
+
extra: prefSlot ? { preferenceSlot: prefSlot } : undefined,
|
|
1075
|
+
narrativeInput: { scope: input.scope, text: input.text },
|
|
1076
|
+
});
|
|
1077
|
+
writeDurableEntry(deps, {
|
|
1078
|
+
text: input.text,
|
|
1079
|
+
vector,
|
|
1080
|
+
category: "preferences",
|
|
1081
|
+
scope: resolvedScope,
|
|
1082
|
+
importance: Math.min(input.importance, 0.5),
|
|
1083
|
+
metadata: prefMetadata,
|
|
1084
|
+
canonicalKey: prefCanonicalKey,
|
|
1085
|
+
source: input.source,
|
|
1086
|
+
language,
|
|
1087
|
+
fts_text,
|
|
1088
|
+
}).catch(() => {}); // Dual-write must never block the primary write
|
|
1089
|
+
}
|
|
1090
|
+
|
|
759
1091
|
return toStoredRecord(input, entry, disposition, canonicalKey, conflictId);
|
|
760
1092
|
}
|
|
761
1093
|
|
|
@@ -768,6 +1100,8 @@ export async function persistWorkflowPattern(
|
|
|
768
1100
|
const tags = mergeTags(input.tags, ["workflow", "pattern"]);
|
|
769
1101
|
const text = buildWorkflowPatternText(input);
|
|
770
1102
|
const vector = await deps.embedder.embedPassage(text);
|
|
1103
|
+
const language = detectLang(text);
|
|
1104
|
+
const fts_text = tokenizeFts(text, language);
|
|
771
1105
|
const canonicalKey = resolveCanonicalKey({
|
|
772
1106
|
category: "patterns",
|
|
773
1107
|
title: input.title,
|
|
@@ -783,6 +1117,8 @@ export async function persistWorkflowPattern(
|
|
|
783
1117
|
metadata: buildWorkflowPatternMetadata(input, tags, canonicalKey),
|
|
784
1118
|
canonicalKey,
|
|
785
1119
|
source: input.source,
|
|
1120
|
+
language,
|
|
1121
|
+
fts_text,
|
|
786
1122
|
});
|
|
787
1123
|
|
|
788
1124
|
return {
|
|
@@ -808,6 +1144,8 @@ export async function persistCaseMemory(
|
|
|
808
1144
|
const tags = mergeTags(input.tags, ["case", "solution"]);
|
|
809
1145
|
const text = buildCaseMemoryText(input);
|
|
810
1146
|
const vector = await deps.embedder.embedPassage(text);
|
|
1147
|
+
const language = detectLang(text);
|
|
1148
|
+
const fts_text = tokenizeFts(text, language);
|
|
811
1149
|
const canonicalKey = resolveCanonicalKey({
|
|
812
1150
|
category: "cases",
|
|
813
1151
|
title: input.title,
|
|
@@ -823,6 +1161,8 @@ export async function persistCaseMemory(
|
|
|
823
1161
|
metadata: buildCaseMemoryMetadata(input, tags, canonicalKey),
|
|
824
1162
|
canonicalKey,
|
|
825
1163
|
source: input.source,
|
|
1164
|
+
language,
|
|
1165
|
+
fts_text,
|
|
826
1166
|
});
|
|
827
1167
|
|
|
828
1168
|
return {
|
|
@@ -869,6 +1209,8 @@ export async function promoteMemory(
|
|
|
869
1209
|
canonicalKey: input.canonicalKey,
|
|
870
1210
|
});
|
|
871
1211
|
const vector = await deps.embedder.embedPassage(text);
|
|
1212
|
+
const language = detectLang(text);
|
|
1213
|
+
const fts_text = tokenizeFts(text, language);
|
|
872
1214
|
const resolvedScope = resolveScope(input);
|
|
873
1215
|
const { entry, disposition, conflictId } = await writeDurableEntry(deps, {
|
|
874
1216
|
text,
|
|
@@ -882,6 +1224,8 @@ export async function promoteMemory(
|
|
|
882
1224
|
source: input.source,
|
|
883
1225
|
sourceCategory: sourceEntry.category,
|
|
884
1226
|
sourceBoundary: boundary,
|
|
1227
|
+
language,
|
|
1228
|
+
fts_text,
|
|
885
1229
|
});
|
|
886
1230
|
|
|
887
1231
|
return {
|
|
@@ -919,6 +1263,11 @@ export async function persistMemoryBatch(
|
|
|
919
1263
|
}),
|
|
920
1264
|
);
|
|
921
1265
|
|
|
1266
|
+
// Note: batch-internal cosine dedup (upstream #319) already runs in the ingest
|
|
1267
|
+
// pipeline (ingest.ts:batchInternalDedup) for large offline batches. For MCP
|
|
1268
|
+
// persistMemoryBatch (typically <10 items), per-item conflict detection (A-2)
|
|
1269
|
+
// in persistMemory handles dedup adequately without double-embedding cost.
|
|
1270
|
+
|
|
922
1271
|
const persisted: StoredMemoryRecord[] = [];
|
|
923
1272
|
for (const memory of normalizedItems) {
|
|
924
1273
|
persisted.push(await persistMemory(deps, memory));
|
package/src/cascade-forget.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import type { MemoryStore, MemoryEntry } from "./store.js";
|
|
22
|
+
import { isActiveMemory } from "./memory-evolution.js";
|
|
22
23
|
|
|
23
24
|
// ---------------------------------------------------------------------------
|
|
24
25
|
// Config
|
|
@@ -85,10 +86,10 @@ export async function cascadeForget(
|
|
|
85
86
|
// Skip the forgotten entry itself (may still be in index)
|
|
86
87
|
if (entry.id === forgottenEntry.id) continue;
|
|
87
88
|
|
|
88
|
-
// Skip already-archived entries
|
|
89
|
+
// Skip already-archived/superseded entries (unified via evolution.status)
|
|
90
|
+
if (!isActiveMemory(entry.metadata)) continue;
|
|
89
91
|
let meta: Record<string, any> = {};
|
|
90
92
|
try { meta = JSON.parse(entry.metadata || "{}"); } catch { /* skip */ }
|
|
91
|
-
if (meta.state === "archived" || meta.state === "superseded") continue;
|
|
92
93
|
|
|
93
94
|
// Proportional demotion: higher similarity → bigger cut
|
|
94
95
|
// At sim=1.0 → full maxDemotion, at threshold → near-zero
|