recallnest 1.4.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/.claude-plugin/CLAUDE.md +29 -0
- package/.claude-plugin/marketplace.json +18 -0
- package/.env.example +3 -0
- package/.github/workflows/ci.yml +28 -0
- package/.mcp.json +9 -0
- package/CHANGELOG.md +51 -0
- package/CLAUDE.md +42 -0
- package/DEMO.md +94 -0
- package/EVAL.md +89 -0
- package/FAILURES.md +75 -0
- package/LICENSE +21 -0
- package/OPERATIONS.md +59 -0
- package/README.md +427 -0
- package/README_CN.md +425 -0
- package/ROADMAP.md +198 -0
- package/assets/recallnest-workbench.png +0 -0
- package/assets/ui/app.js +807 -0
- package/assets/ui/index.html +154 -0
- package/assets/ui/styles.css +605 -0
- package/assets/wechat-qr.jpg +0 -0
- package/config.json.example +38 -0
- package/daily-recall.sh +87 -0
- package/daily-reflections/2026-03-05.md +15 -0
- package/daily-reflections/2026-03-06.md +15 -0
- package/daily-reflections/2026-03-07.md +7 -0
- package/daily-reflections/2026-03-08.md +34 -0
- package/daily-reflections/2026-03-09.md +35 -0
- package/daily-reflections/2026-03-10.md +36 -0
- package/daily-reflections/2026-03-11.md +35 -0
- package/daily-reflections/2026-03-12.md +36 -0
- package/daily-reflections/2026-03-13.md +35 -0
- package/daily-reflections/2026-03-14.md +35 -0
- package/daily-reflections/2026-03-15.md +35 -0
- package/daily-reflections/2026-03-16.md +41 -0
- package/daily-reflections/2026-03-17.md +10 -0
- package/daily-reflections/2026-03-18.md +10 -0
- package/daily-reflections/2026-03-19.md +10 -0
- package/daily-reflections/2026-03-20.md +10 -0
- package/daily-reflections/2026-03-21.md +10 -0
- package/daily-reflections/2026-03-22.md +10 -0
- package/daily-reflections/2026-03-23.md +10 -0
- package/daily-reflections/2026-03-24.md +10 -0
- package/daily-reflections/2026-03-25.md +32 -0
- package/daily-reflections/2026-03-26.md +37 -0
- package/daily-reflections/2026-03-27.md +35 -0
- package/daily-reflections/2026-03-28.md +35 -0
- package/daily-reflections/2026-03-29.md +35 -0
- package/daily-reflections/2026-03-30.md +36 -0
- package/daily-reflections/2026-03-31.md +35 -0
- package/daily-reflections/2026-04-01.md +35 -0
- package/docs/api-reference.md +1291 -0
- package/docs/architecture.md +168 -0
- package/docs/memory-boundary-contract.md +226 -0
- package/docs/memory-categories.md +119 -0
- package/docs/pipeline-audit-ultramemory.md +177 -0
- package/docs/plans/2026-03-08-query-expansion.md +270 -0
- package/docs/plans/2026-03-16-continuity-layer.md +196 -0
- package/docs/plans/mcp-tiering-plan.md +126 -0
- package/docs/research/recall-quality-audit-2026-03-22.md +153 -0
- package/docs/self-evolution.md +198 -0
- package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +782 -0
- package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +77 -0
- package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +95 -0
- package/eval/cases.json +85 -0
- package/eval/continuity/case-seeds.json +90 -0
- package/eval/continuity/cases.json +302 -0
- package/eval/continuity/memory-seeds.json +26 -0
- package/eval/continuity/pattern-seeds.json +100 -0
- package/eval/continuity/results-2026-03-16.md +117 -0
- package/eval/continuity/results-2026-03-19.md +453 -0
- package/eval/continuity/runbook.md +110 -0
- package/eval/continuity/seed-cases.md +54 -0
- package/eval/reports/2026-03-06-abstract-query-baseline.md +96 -0
- package/eval/reports/2026-03-06-aws-ssh-baseline.md +82 -0
- package/eval/reports/2026-03-06-baseline.md +82 -0
- package/eval/reports/query-expansion.md +138 -0
- package/integrations/README.md +57 -0
- package/integrations/claude-code/README.md +133 -0
- package/integrations/claude-code/claude-md-snippet.md +31 -0
- package/integrations/claude-code/setup.sh +66 -0
- package/integrations/claude-code/smoke-continuity.sh +452 -0
- package/integrations/codex/README.md +49 -0
- package/integrations/codex/agents-md-snippet.md +31 -0
- package/integrations/codex/setup.sh +58 -0
- package/integrations/examples/claude-agent-sdk/memory-agent.ts +182 -0
- package/integrations/examples/langchain/memory-chain.py +111 -0
- package/integrations/examples/openai-agents-sdk/memory-agent.py +109 -0
- package/integrations/gemini-cli/README.md +57 -0
- package/integrations/gemini-cli/gemini-md-snippet.md +31 -0
- package/integrations/gemini-cli/setup.sh +64 -0
- package/integrations/shared/install-managed-block.sh +51 -0
- package/lm +19 -0
- package/package.json +50 -0
- package/plan-v1.2-distributable.md +105 -0
- package/scripts/archive-distilled-junk.ts +116 -0
- package/scripts/backfill-l0l1l2.ts +98 -0
- package/scripts/blind-scan.ts +588 -0
- package/scripts/cleanup-step1-exact-dups.ts +117 -0
- package/scripts/cleanup-step2-template-dups.ts +113 -0
- package/scripts/cleanup-step3-stale-content.ts +149 -0
- package/scripts/cleanup-step4-orphan-scopes.ts +131 -0
- package/scripts/delete-distilled-junk.ts +88 -0
- package/scripts/distill-facts.ts +305 -0
- package/scripts/health-check.ts +343 -0
- package/scripts/incremental-ingest.sh +71 -0
- package/scripts/mlp-health-check.ts +382 -0
- package/scripts/pull-claude-desktop.ts +515 -0
- package/scripts/re-ingest-cc.sh +66 -0
- package/scripts/rebalance-memory-health.ts +181 -0
- package/scripts/run-desktop-ingest.sh +11 -0
- package/scripts/start-server.sh +12 -0
- package/scripts/viz-dashboard.html +423 -0
- package/scripts/viz-export.ts +703 -0
- package/src/__tests__/access-tracker-novelty.test.ts +228 -0
- package/src/__tests__/auto-consolidation.test.ts +168 -0
- package/src/__tests__/auto-recall.test.ts +131 -0
- package/src/__tests__/batch-dedup.test.ts +65 -0
- package/src/__tests__/capture-engine.test.ts +1142 -0
- package/src/__tests__/capture-heuristic.test.ts +171 -0
- package/src/__tests__/cascade-forget.test.ts +210 -0
- package/src/__tests__/category-min-scores.test.ts +33 -0
- package/src/__tests__/confidence-tracker.test.ts +196 -0
- package/src/__tests__/conflict-advisor.test.ts +206 -0
- package/src/__tests__/conflict-escalation.test.ts +149 -0
- package/src/__tests__/conflict-lifecycle.test.ts +83 -0
- package/src/__tests__/conflict-output.test.ts +85 -0
- package/src/__tests__/conflict-store.test.ts +374 -0
- package/src/__tests__/consolidation-engine.test.ts +181 -0
- package/src/__tests__/context-composer-pins.test.ts +64 -0
- package/src/__tests__/context-composer-task-selection.test.ts +24 -0
- package/src/__tests__/context-renderer.test.ts +91 -0
- package/src/__tests__/continuity-eval.test.ts +275 -0
- package/src/__tests__/continuity-schema.test.ts +97 -0
- package/src/__tests__/core-recall-eval.test.ts +235 -0
- package/src/__tests__/core-summary.test.ts +109 -0
- package/src/__tests__/dedup-actions.test.ts +58 -0
- package/src/__tests__/deterministic-id.test.ts +39 -0
- package/src/__tests__/doctor.test.ts +303 -0
- package/src/__tests__/drill-down.test.ts +108 -0
- package/src/__tests__/embedder.test.ts +122 -0
- package/src/__tests__/entity-resolver.test.ts +58 -0
- package/src/__tests__/event-segmenter.test.ts +153 -0
- package/src/__tests__/hotness-blend.test.ts +76 -0
- package/src/__tests__/ingest.test.ts +360 -0
- package/src/__tests__/kg-extractor.test.ts +184 -0
- package/src/__tests__/kg-store.test.ts +198 -0
- package/src/__tests__/llm-consolidation.test.ts +226 -0
- package/src/__tests__/memory-boundaries.test.ts +237 -0
- package/src/__tests__/memory-health-rebalance.test.ts +144 -0
- package/src/__tests__/memory-output.test.ts +215 -0
- package/src/__tests__/multi-vector.test.ts +242 -0
- package/src/__tests__/ppr-traversal.test.ts +256 -0
- package/src/__tests__/preference-guard-smoke.test.ts +211 -0
- package/src/__tests__/preference-matcher.test.ts +230 -0
- package/src/__tests__/preference-slots.test.ts +106 -0
- package/src/__tests__/prospective-memory.test.ts +283 -0
- package/src/__tests__/query-entity-detector.test.ts +171 -0
- package/src/__tests__/query-expander.test.ts +34 -0
- package/src/__tests__/recall-mode.test.ts +64 -0
- package/src/__tests__/result-synthesizer.test.ts +198 -0
- package/src/__tests__/resume-context.test.ts +4743 -0
- package/src/__tests__/retrieval-trace.test.ts +213 -0
- package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
- package/src/__tests__/retroactive-boost.test.ts +223 -0
- package/src/__tests__/scope-inventory.test.ts +200 -0
- package/src/__tests__/scope-policy.test.ts +69 -0
- package/src/__tests__/session-checkpoint.test.ts +259 -0
- package/src/__tests__/session-gc.test.ts +215 -0
- package/src/__tests__/store-scope-audit.test.ts +81 -0
- package/src/__tests__/temporal-parser.test.ts +241 -0
- package/src/__tests__/term-registry.test.ts +121 -0
- package/src/__tests__/tool-output-compressor.test.ts +133 -0
- package/src/__tests__/version-manager.test.ts +213 -0
- package/src/__tests__/workflow-observation.test.ts +400 -0
- package/src/access-tracker.ts +316 -0
- package/src/adaptive-retrieval.ts +64 -0
- package/src/api-server.ts +667 -0
- package/src/asset-sync.ts +79 -0
- package/src/auto-consolidation.ts +133 -0
- package/src/auto-gc.ts +124 -0
- package/src/auto-recall.ts +95 -0
- package/src/capture-engine.ts +931 -0
- package/src/capture-heuristic.ts +177 -0
- package/src/cascade-forget.ts +135 -0
- package/src/chunker.ts +253 -0
- package/src/cli.ts +1917 -0
- package/src/confidence-tracker.ts +166 -0
- package/src/conflict-advisor.ts +433 -0
- package/src/conflict-engine.ts +181 -0
- package/src/conflict-escalation.ts +138 -0
- package/src/conflict-lifecycle.ts +67 -0
- package/src/conflict-output.ts +218 -0
- package/src/conflict-schema.ts +130 -0
- package/src/conflict-store.ts +127 -0
- package/src/consolidation-engine.ts +256 -0
- package/src/context-composer-pins.ts +183 -0
- package/src/context-composer-queries.ts +121 -0
- package/src/context-composer-scope.ts +56 -0
- package/src/context-composer-stable-selection.ts +200 -0
- package/src/context-composer-stable.ts +217 -0
- package/src/context-composer-task-fallbacks.ts +36 -0
- package/src/context-composer-task-ranking.ts +197 -0
- package/src/context-composer-task-results.ts +137 -0
- package/src/context-composer-task-selection.ts +342 -0
- package/src/context-composer-text.ts +57 -0
- package/src/context-composer.ts +300 -0
- package/src/context-renderer.ts +179 -0
- package/src/decay-engine.ts +189 -0
- package/src/doctor.ts +543 -0
- package/src/embedder.ts +557 -0
- package/src/entity-resolver.ts +131 -0
- package/src/eval.ts +632 -0
- package/src/event-segmenter.ts +216 -0
- package/src/ingest.ts +1822 -0
- package/src/kg-extractor.ts +217 -0
- package/src/kg-store.ts +353 -0
- package/src/legacy-scope-review.ts +53 -0
- package/src/llm-client.ts +629 -0
- package/src/llm-consolidation.ts +168 -0
- package/src/mcp-server.ts +1337 -0
- package/src/memory-assets.ts +475 -0
- package/src/memory-boundaries.ts +354 -0
- package/src/memory-health-rebalance.ts +220 -0
- package/src/memory-output.ts +474 -0
- package/src/memory-schema.ts +184 -0
- package/src/multi-vector.ts +234 -0
- package/src/noise-filter.ts +155 -0
- package/src/ppr-traversal.ts +279 -0
- package/src/preference-matcher.ts +174 -0
- package/src/preference-slots.ts +259 -0
- package/src/prospective-memory.ts +198 -0
- package/src/query-entity-detector.ts +207 -0
- package/src/query-expander.ts +98 -0
- package/src/result-synthesizer.ts +114 -0
- package/src/retrieval-profiles.ts +101 -0
- package/src/retrieval-trace.ts +94 -0
- package/src/retriever.ts +1338 -0
- package/src/retroactive-boost.ts +154 -0
- package/src/rif.ts +73 -0
- package/src/runtime-config.ts +172 -0
- package/src/schema-utils.ts +56 -0
- package/src/scope-inventory.ts +310 -0
- package/src/scope-policy.ts +142 -0
- package/src/session-engine.ts +168 -0
- package/src/session-output.ts +96 -0
- package/src/session-schema.ts +60 -0
- package/src/session-store.ts +293 -0
- package/src/stderr-log.ts +16 -0
- package/src/store.ts +903 -0
- package/src/temporal-parser.ts +318 -0
- package/src/term-registry.ts +791 -0
- package/src/tool-output-compressor.ts +115 -0
- package/src/tracker.ts +57 -0
- package/src/ui-server.ts +430 -0
- package/src/version-manager.ts +172 -0
- package/src/workflow-observation-engine.ts +316 -0
- package/src/workflow-observation-managed.ts +83 -0
- package/src/workflow-observation-output.ts +86 -0
- package/src/workflow-observation-schema.ts +87 -0
- package/src/workflow-observation-scope-review.ts +409 -0
- package/src/workflow-observation-store.ts +69 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import type { DurableMemoryCategory } from "./memory-schema.js";
|
|
2
|
+
import { inferPreferenceSlot } from "./preference-slots.js";
|
|
3
|
+
|
|
4
|
+
export const MEMORY_LAYERS = [
|
|
5
|
+
"canonical",
|
|
6
|
+
"durable",
|
|
7
|
+
"session",
|
|
8
|
+
"evidence",
|
|
9
|
+
] as const;
|
|
10
|
+
|
|
11
|
+
export const MEMORY_AUTHORITIES = [
|
|
12
|
+
"manual-document",
|
|
13
|
+
"structured-memory",
|
|
14
|
+
"document-ingest",
|
|
15
|
+
"transcript-ingest",
|
|
16
|
+
"session-checkpoint",
|
|
17
|
+
"distillation",
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
export const MEMORY_CONFLICT_POLICIES = [
|
|
21
|
+
"latest-wins",
|
|
22
|
+
"append-only",
|
|
23
|
+
"manual-review",
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
export type MemoryLayer = (typeof MEMORY_LAYERS)[number];
|
|
27
|
+
export type MemoryAuthority = (typeof MEMORY_AUTHORITIES)[number];
|
|
28
|
+
export type MemoryConflictPolicy = (typeof MEMORY_CONFLICT_POLICIES)[number];
|
|
29
|
+
|
|
30
|
+
export interface MemoryBoundaryMetadata {
|
|
31
|
+
layer: MemoryLayer;
|
|
32
|
+
authority: MemoryAuthority;
|
|
33
|
+
conflictPolicy: MemoryConflictPolicy;
|
|
34
|
+
originalCategory?: DurableMemoryCategory;
|
|
35
|
+
downgradedFrom?: DurableMemoryCategory;
|
|
36
|
+
note?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface PromotedFromMetadata {
|
|
40
|
+
memoryId: string;
|
|
41
|
+
scope?: string;
|
|
42
|
+
category?: string;
|
|
43
|
+
source?: string;
|
|
44
|
+
boundary?: MemoryBoundaryMetadata | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ProvenanceHistoryMetadata extends PromotedFromMetadata {
|
|
48
|
+
observedAt?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface MemoryProvenance {
|
|
52
|
+
boundary: MemoryBoundaryMetadata | null;
|
|
53
|
+
canonicalKey: string | null;
|
|
54
|
+
promotedFrom: PromotedFromMetadata | null;
|
|
55
|
+
provenanceHistory: ProvenanceHistoryMetadata[];
|
|
56
|
+
provenanceHistoryCount: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface IngestBoundaryResolution {
|
|
60
|
+
category: DurableMemoryCategory;
|
|
61
|
+
boundary: MemoryBoundaryMetadata;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const TRANSCRIPT_SOURCES = new Set(["cc", "codex", "gemini"]);
|
|
65
|
+
const TRANSCRIPT_SCOPE_PREFIXES = ["cc:", "codex:", "gemini:"];
|
|
66
|
+
|
|
67
|
+
export function getConflictPolicyForCategory(category: DurableMemoryCategory): MemoryConflictPolicy {
|
|
68
|
+
return category === "events" || category === "cases"
|
|
69
|
+
? "append-only"
|
|
70
|
+
: "latest-wins";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function isTranscriptIngestSource(source: string): boolean {
|
|
74
|
+
return TRANSCRIPT_SOURCES.has(source);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function isTranscriptScope(scope: string): boolean {
|
|
78
|
+
return TRANSCRIPT_SCOPE_PREFIXES.some((prefix) => scope.startsWith(prefix));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function isDurableMemoryScope(scope: string): boolean {
|
|
82
|
+
return scope.startsWith("memory:") || scope.startsWith("asset:");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function buildStructuredMemoryBoundary(
|
|
86
|
+
category: DurableMemoryCategory,
|
|
87
|
+
): MemoryBoundaryMetadata {
|
|
88
|
+
return {
|
|
89
|
+
layer: "durable",
|
|
90
|
+
authority: "structured-memory",
|
|
91
|
+
conflictPolicy: getConflictPolicyForCategory(category),
|
|
92
|
+
originalCategory: category,
|
|
93
|
+
note: "Structured memory writes are the durable source inside RecallNest.",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function buildDistillationBoundary(
|
|
98
|
+
category: DurableMemoryCategory,
|
|
99
|
+
): MemoryBoundaryMetadata {
|
|
100
|
+
return {
|
|
101
|
+
layer: "durable",
|
|
102
|
+
authority: "distillation",
|
|
103
|
+
conflictPolicy: getConflictPolicyForCategory(category),
|
|
104
|
+
originalCategory: category,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function normalizeCanonicalKey(value: string): string {
|
|
109
|
+
return value
|
|
110
|
+
.trim()
|
|
111
|
+
.toLowerCase()
|
|
112
|
+
.replace(/['"`]/g, "")
|
|
113
|
+
.replace(/[^a-z0-9\p{Script=Han}]+/gu, "-")
|
|
114
|
+
.replace(/^-+|-+$/g, "")
|
|
115
|
+
.replace(/-{2,}/g, "-")
|
|
116
|
+
.slice(0, 120);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function collapseKeyText(value: string): string {
|
|
120
|
+
return value
|
|
121
|
+
.replace(/\s+/g, " ")
|
|
122
|
+
.trim();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function buildDefaultCanonicalKey(params: {
|
|
126
|
+
category: DurableMemoryCategory;
|
|
127
|
+
text?: string;
|
|
128
|
+
title?: string;
|
|
129
|
+
}): string {
|
|
130
|
+
if (params.category === "preferences") {
|
|
131
|
+
const slot = inferPreferenceSlot(params.text || params.title || "");
|
|
132
|
+
if (slot) {
|
|
133
|
+
return truncateCanonicalKey(
|
|
134
|
+
slot.type === "brand-item"
|
|
135
|
+
? [params.category, slot.type, slot.brand, slot.item]
|
|
136
|
+
: slot.type === "reply-style"
|
|
137
|
+
? [params.category, slot.type, ...slot.traits]
|
|
138
|
+
: [params.category, slot.type, slot.preferredTool, "over", slot.avoidedTool],
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const base = collapseKeyText(params.title || params.text || "");
|
|
144
|
+
const normalizedBase = normalizeCanonicalKey(base);
|
|
145
|
+
return normalizedBase
|
|
146
|
+
? `${params.category}:${normalizedBase}`
|
|
147
|
+
: `${params.category}:memory`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function truncateCanonicalKey(segments: string[]): string {
|
|
151
|
+
return segments
|
|
152
|
+
.filter(Boolean)
|
|
153
|
+
.join(":")
|
|
154
|
+
.slice(0, 120)
|
|
155
|
+
.replace(/[:\-]+$/u, "");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function resolveIngestBoundary(params: {
|
|
159
|
+
source: string;
|
|
160
|
+
scope: string;
|
|
161
|
+
category: DurableMemoryCategory;
|
|
162
|
+
}): IngestBoundaryResolution {
|
|
163
|
+
const { source, scope, category } = params;
|
|
164
|
+
|
|
165
|
+
if (isTranscriptIngestSource(source)) {
|
|
166
|
+
if (category === "profile" || category === "preferences") {
|
|
167
|
+
return {
|
|
168
|
+
category: "events",
|
|
169
|
+
boundary: {
|
|
170
|
+
layer: "evidence",
|
|
171
|
+
authority: "transcript-ingest",
|
|
172
|
+
conflictPolicy: "append-only",
|
|
173
|
+
originalCategory: category,
|
|
174
|
+
downgradedFrom: category,
|
|
175
|
+
note: "Transcript-derived stable facts stay as evidence until explicitly promoted.",
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
category,
|
|
182
|
+
boundary: {
|
|
183
|
+
layer: "evidence",
|
|
184
|
+
authority: "transcript-ingest",
|
|
185
|
+
conflictPolicy: getConflictPolicyForCategory(category),
|
|
186
|
+
originalCategory: category,
|
|
187
|
+
note: "Transcript-derived memories are evidence and should not override curated memory.",
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (isDurableMemoryScope(scope) || scope === "memory") {
|
|
193
|
+
return {
|
|
194
|
+
category,
|
|
195
|
+
boundary: {
|
|
196
|
+
layer: "durable",
|
|
197
|
+
authority: "document-ingest",
|
|
198
|
+
conflictPolicy: getConflictPolicyForCategory(category),
|
|
199
|
+
originalCategory: category,
|
|
200
|
+
note: "Curated memory documents can mirror durable memory, but they are not session authority.",
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
category,
|
|
207
|
+
boundary: {
|
|
208
|
+
layer: "evidence",
|
|
209
|
+
authority: "document-ingest",
|
|
210
|
+
conflictPolicy: getConflictPolicyForCategory(category),
|
|
211
|
+
originalCategory: category,
|
|
212
|
+
note: "Unstructured ingest stays evidence until explicitly promoted.",
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function parseMetadataObject(metadata?: string): Record<string, unknown> | null {
|
|
218
|
+
if (!metadata) return null;
|
|
219
|
+
try {
|
|
220
|
+
const parsed = JSON.parse(metadata);
|
|
221
|
+
return parsed && typeof parsed === "object" ? parsed as Record<string, unknown> : null;
|
|
222
|
+
} catch {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function coerceBoundaryMetadata(candidate: unknown): MemoryBoundaryMetadata | null {
|
|
228
|
+
if (!candidate || typeof candidate !== "object") return null;
|
|
229
|
+
|
|
230
|
+
const record = candidate as Record<string, unknown>;
|
|
231
|
+
const layer = typeof record.layer === "string" ? record.layer : "";
|
|
232
|
+
const authority = typeof record.authority === "string" ? record.authority : "";
|
|
233
|
+
const conflictPolicy = typeof record.conflictPolicy === "string" ? record.conflictPolicy : "";
|
|
234
|
+
|
|
235
|
+
if (
|
|
236
|
+
!MEMORY_LAYERS.includes(layer as MemoryLayer) ||
|
|
237
|
+
!MEMORY_AUTHORITIES.includes(authority as MemoryAuthority) ||
|
|
238
|
+
!MEMORY_CONFLICT_POLICIES.includes(conflictPolicy as MemoryConflictPolicy)
|
|
239
|
+
) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
layer: layer as MemoryLayer,
|
|
245
|
+
authority: authority as MemoryAuthority,
|
|
246
|
+
conflictPolicy: conflictPolicy as MemoryConflictPolicy,
|
|
247
|
+
...(typeof record.originalCategory === "string"
|
|
248
|
+
? { originalCategory: record.originalCategory as DurableMemoryCategory }
|
|
249
|
+
: {}),
|
|
250
|
+
...(typeof record.downgradedFrom === "string"
|
|
251
|
+
? { downgradedFrom: record.downgradedFrom as DurableMemoryCategory }
|
|
252
|
+
: {}),
|
|
253
|
+
...(typeof record.note === "string" ? { note: record.note } : {}),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function extractBoundaryMetadata(metadata?: string): MemoryBoundaryMetadata | null {
|
|
258
|
+
const parsed = parseMetadataObject(metadata);
|
|
259
|
+
return coerceBoundaryMetadata(parsed?.boundary);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function coercePromotedFromMetadata(candidate: unknown): PromotedFromMetadata | null {
|
|
263
|
+
if (!candidate || typeof candidate !== "object") return null;
|
|
264
|
+
|
|
265
|
+
const record = candidate as Record<string, unknown>;
|
|
266
|
+
const memoryId = typeof record.memoryId === "string" ? record.memoryId.trim() : "";
|
|
267
|
+
if (!memoryId) return null;
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
memoryId,
|
|
271
|
+
...(typeof record.scope === "string" ? { scope: record.scope } : {}),
|
|
272
|
+
...(typeof record.category === "string" ? { category: record.category } : {}),
|
|
273
|
+
...(typeof record.source === "string" ? { source: record.source } : {}),
|
|
274
|
+
boundary: coerceBoundaryMetadata(record.boundary),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function coerceProvenanceHistoryMetadata(candidate: unknown): ProvenanceHistoryMetadata | null {
|
|
279
|
+
const promotedFrom = coercePromotedFromMetadata(candidate);
|
|
280
|
+
if (!promotedFrom || typeof candidate !== "object") return null;
|
|
281
|
+
|
|
282
|
+
const record = candidate as Record<string, unknown>;
|
|
283
|
+
return {
|
|
284
|
+
...promotedFrom,
|
|
285
|
+
...(typeof record.observedAt === "string" ? { observedAt: record.observedAt } : {}),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function extractPromotedFrom(metadata?: string): PromotedFromMetadata | null {
|
|
290
|
+
const parsed = parseMetadataObject(metadata);
|
|
291
|
+
return coercePromotedFromMetadata(parsed?.promotedFrom);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function extractProvenanceHistory(metadata?: string): ProvenanceHistoryMetadata[] {
|
|
295
|
+
const parsed = parseMetadataObject(metadata);
|
|
296
|
+
const rawHistory = Array.isArray(parsed?.provenanceHistory) ? parsed.provenanceHistory : [];
|
|
297
|
+
const history = rawHistory
|
|
298
|
+
.map((candidate) => coerceProvenanceHistoryMetadata(candidate))
|
|
299
|
+
.filter((candidate): candidate is ProvenanceHistoryMetadata => Boolean(candidate));
|
|
300
|
+
|
|
301
|
+
if (history.length > 0) {
|
|
302
|
+
return history;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const promotedFrom = coerceProvenanceHistoryMetadata(parsed?.promotedFrom);
|
|
306
|
+
return promotedFrom ? [promotedFrom] : [];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function extractProvenanceHistoryCount(metadata?: string): number {
|
|
310
|
+
const parsed = parseMetadataObject(metadata);
|
|
311
|
+
const candidate = parsed?.provenanceHistoryCount;
|
|
312
|
+
if (typeof candidate === "number" && Number.isFinite(candidate) && candidate >= 0) {
|
|
313
|
+
return Math.floor(candidate);
|
|
314
|
+
}
|
|
315
|
+
return extractProvenanceHistory(metadata).length;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function extractMemoryProvenance(params: {
|
|
319
|
+
scope: string;
|
|
320
|
+
metadata?: string;
|
|
321
|
+
}): MemoryProvenance {
|
|
322
|
+
return {
|
|
323
|
+
boundary: extractBoundaryMetadata(params.metadata),
|
|
324
|
+
canonicalKey: extractCanonicalKey(params.metadata),
|
|
325
|
+
promotedFrom: extractPromotedFrom(params.metadata),
|
|
326
|
+
provenanceHistory: extractProvenanceHistory(params.metadata),
|
|
327
|
+
provenanceHistoryCount: extractProvenanceHistoryCount(params.metadata),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function extractCanonicalKey(metadata?: string): string | null {
|
|
332
|
+
const parsed = parseMetadataObject(metadata);
|
|
333
|
+
const canonicalKey = parsed?.canonicalKey;
|
|
334
|
+
return typeof canonicalKey === "string" && canonicalKey.trim().length > 0
|
|
335
|
+
? canonicalKey
|
|
336
|
+
: null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function shouldUseStableMemoryResult(params: {
|
|
340
|
+
category: "profile" | "preferences" | "entities";
|
|
341
|
+
scope: string;
|
|
342
|
+
metadata?: string;
|
|
343
|
+
}): boolean {
|
|
344
|
+
if (isTranscriptScope(params.scope)) {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const boundary = extractBoundaryMetadata(params.metadata);
|
|
349
|
+
if (!boundary) {
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return boundary.layer !== "evidence";
|
|
354
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import type { MemoryTier } from "./decay-engine.js";
|
|
2
|
+
|
|
3
|
+
export type StoredMemoryTier = MemoryTier | "unknown";
|
|
4
|
+
|
|
5
|
+
export interface MemoryHealthSnapshot {
|
|
6
|
+
id: string;
|
|
7
|
+
importance: number;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
metadata?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface MemoryHealthRebalancePlan {
|
|
13
|
+
id: string;
|
|
14
|
+
accessCount: number;
|
|
15
|
+
currentImportance: number;
|
|
16
|
+
nextImportance: number;
|
|
17
|
+
currentTier: StoredMemoryTier;
|
|
18
|
+
targetTier: MemoryTier;
|
|
19
|
+
currentMetadata: Record<string, unknown>;
|
|
20
|
+
nextMetadata: Record<string, unknown>;
|
|
21
|
+
tierChanged: boolean;
|
|
22
|
+
tierBackfilled: boolean;
|
|
23
|
+
deadMemoryRow: boolean;
|
|
24
|
+
deadMemoryDemoted: boolean;
|
|
25
|
+
importanceChanged: boolean;
|
|
26
|
+
changed: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface MemoryHealthRebalanceSummary {
|
|
30
|
+
totalRows: number;
|
|
31
|
+
changedRows: number;
|
|
32
|
+
deadMemoryRows: number;
|
|
33
|
+
deadMemoryDemotions: number;
|
|
34
|
+
tierBackfills: number;
|
|
35
|
+
tierChanges: number;
|
|
36
|
+
importanceChanges: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface MemoryHealthDatasetStats {
|
|
40
|
+
maxAccessCount: number;
|
|
41
|
+
minTimestamp: number;
|
|
42
|
+
maxTimestamp: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface ImportanceBand {
|
|
46
|
+
min: number;
|
|
47
|
+
max: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const IMPORTANCE_BANDS: Record<MemoryTier, ImportanceBand> = {
|
|
51
|
+
core: { min: 0.85, max: 0.95 },
|
|
52
|
+
working: { min: 0.6, max: 0.8 },
|
|
53
|
+
peripheral: { min: 0.3, max: 0.5 },
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function clamp(value: number, min: number, max: number): number {
|
|
57
|
+
if (!Number.isFinite(value)) return min;
|
|
58
|
+
return Math.min(max, Math.max(min, value));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function roundImportance(value: number): number {
|
|
62
|
+
return Math.round(value * 1000) / 1000;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function parseMemoryHealthMetadata(metadata?: string): Record<string, unknown> {
|
|
66
|
+
try {
|
|
67
|
+
const parsed = JSON.parse(metadata || "{}");
|
|
68
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
69
|
+
? parsed as Record<string, unknown>
|
|
70
|
+
: {};
|
|
71
|
+
} catch {
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getMemoryHealthAccessCount(metadata: Record<string, unknown>): number {
|
|
77
|
+
const camel = metadata.accessCount;
|
|
78
|
+
if (typeof camel === "number" && Number.isFinite(camel) && camel >= 0) {
|
|
79
|
+
return Math.floor(camel);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const snake = metadata.access_count;
|
|
83
|
+
if (typeof snake === "number" && Number.isFinite(snake) && snake >= 0) {
|
|
84
|
+
return Math.floor(snake);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function getStoredMemoryTier(metadata: Record<string, unknown>): StoredMemoryTier {
|
|
91
|
+
const tier = metadata.tier;
|
|
92
|
+
return tier === "core" || tier === "working" || tier === "peripheral"
|
|
93
|
+
? tier
|
|
94
|
+
: "unknown";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function resolveRebalancedTier(currentTier: StoredMemoryTier, accessCount: number): MemoryTier {
|
|
98
|
+
if (accessCount <= 0) return "peripheral";
|
|
99
|
+
if (currentTier !== "unknown") return currentTier;
|
|
100
|
+
if (accessCount > 5) return "core";
|
|
101
|
+
return "working";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function computeTierAccessSignal(
|
|
105
|
+
tier: MemoryTier,
|
|
106
|
+
accessCount: number,
|
|
107
|
+
maxAccessCount: number,
|
|
108
|
+
): number {
|
|
109
|
+
if (accessCount <= 0) return 0;
|
|
110
|
+
|
|
111
|
+
switch (tier) {
|
|
112
|
+
case "peripheral": {
|
|
113
|
+
const ceiling = Math.max(1, Math.min(5, maxAccessCount));
|
|
114
|
+
return clamp(accessCount / ceiling, 0, 1);
|
|
115
|
+
}
|
|
116
|
+
case "working": {
|
|
117
|
+
if (accessCount <= 1) return 0;
|
|
118
|
+
return clamp((Math.min(accessCount, 5) - 1) / 4, 0, 1);
|
|
119
|
+
}
|
|
120
|
+
case "core": {
|
|
121
|
+
const ceiling = Math.max(6, maxAccessCount);
|
|
122
|
+
if (ceiling <= 6) return 1;
|
|
123
|
+
return clamp((Math.min(accessCount, ceiling) - 6) / (ceiling - 6), 0, 1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function computeRebalancedImportance(
|
|
129
|
+
currentImportance: number,
|
|
130
|
+
tier: MemoryTier,
|
|
131
|
+
accessCount: number,
|
|
132
|
+
maxAccessCount: number,
|
|
133
|
+
recencySignal: number,
|
|
134
|
+
): number {
|
|
135
|
+
const band = IMPORTANCE_BANDS[tier];
|
|
136
|
+
const span = band.max - band.min;
|
|
137
|
+
const currentSignal = clamp(currentImportance, 0, 1);
|
|
138
|
+
const accessSignal = computeTierAccessSignal(tier, accessCount, maxAccessCount);
|
|
139
|
+
const blendedSignal = tier === "peripheral" && accessCount === 0
|
|
140
|
+
? clamp(currentSignal * 0.15 + recencySignal * 0.85, 0, 1)
|
|
141
|
+
: clamp(currentSignal * 0.25 + accessSignal * 0.65 + recencySignal * 0.1, 0, 1);
|
|
142
|
+
return roundImportance(clamp(band.min + span * blendedSignal, band.min, band.max));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function buildMemoryHealthRebalancePlan(
|
|
146
|
+
snapshot: MemoryHealthSnapshot,
|
|
147
|
+
stats: MemoryHealthDatasetStats,
|
|
148
|
+
): MemoryHealthRebalancePlan {
|
|
149
|
+
const currentMetadata = parseMemoryHealthMetadata(snapshot.metadata);
|
|
150
|
+
const accessCount = getMemoryHealthAccessCount(currentMetadata);
|
|
151
|
+
const currentTier = getStoredMemoryTier(currentMetadata);
|
|
152
|
+
const targetTier = resolveRebalancedTier(currentTier, accessCount);
|
|
153
|
+
const currentImportance = clamp(snapshot.importance, 0, 1);
|
|
154
|
+
const recencySignal = stats.maxTimestamp <= stats.minTimestamp
|
|
155
|
+
? 0.5
|
|
156
|
+
: clamp(
|
|
157
|
+
(snapshot.timestamp - stats.minTimestamp) / (stats.maxTimestamp - stats.minTimestamp),
|
|
158
|
+
0,
|
|
159
|
+
1,
|
|
160
|
+
);
|
|
161
|
+
const nextImportance = computeRebalancedImportance(
|
|
162
|
+
currentImportance,
|
|
163
|
+
targetTier,
|
|
164
|
+
accessCount,
|
|
165
|
+
stats.maxAccessCount,
|
|
166
|
+
recencySignal,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const nextMetadata: Record<string, unknown> = {
|
|
170
|
+
...currentMetadata,
|
|
171
|
+
tier: targetTier,
|
|
172
|
+
importance: nextImportance,
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const tierChanged = currentTier !== targetTier;
|
|
176
|
+
const tierBackfilled = currentTier === "unknown";
|
|
177
|
+
const deadMemoryRow = accessCount === 0;
|
|
178
|
+
const deadMemoryDemoted = deadMemoryRow && currentTier !== "peripheral";
|
|
179
|
+
const importanceChanged = roundImportance(currentImportance) !== nextImportance;
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
id: snapshot.id,
|
|
183
|
+
accessCount,
|
|
184
|
+
currentImportance,
|
|
185
|
+
nextImportance,
|
|
186
|
+
currentTier,
|
|
187
|
+
targetTier,
|
|
188
|
+
currentMetadata,
|
|
189
|
+
nextMetadata,
|
|
190
|
+
tierChanged,
|
|
191
|
+
tierBackfilled,
|
|
192
|
+
deadMemoryRow,
|
|
193
|
+
deadMemoryDemoted,
|
|
194
|
+
importanceChanged,
|
|
195
|
+
changed: tierChanged || importanceChanged || !("importance" in currentMetadata),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function summarizeMemoryHealthPlans(
|
|
200
|
+
plans: MemoryHealthRebalancePlan[],
|
|
201
|
+
): MemoryHealthRebalanceSummary {
|
|
202
|
+
return plans.reduce<MemoryHealthRebalanceSummary>((summary, plan) => {
|
|
203
|
+
summary.totalRows += 1;
|
|
204
|
+
if (plan.changed) summary.changedRows += 1;
|
|
205
|
+
if (plan.deadMemoryRow) summary.deadMemoryRows += 1;
|
|
206
|
+
if (plan.deadMemoryDemoted) summary.deadMemoryDemotions += 1;
|
|
207
|
+
if (plan.tierBackfilled) summary.tierBackfills += 1;
|
|
208
|
+
if (plan.tierChanged) summary.tierChanges += 1;
|
|
209
|
+
if (plan.importanceChanged) summary.importanceChanges += 1;
|
|
210
|
+
return summary;
|
|
211
|
+
}, {
|
|
212
|
+
totalRows: 0,
|
|
213
|
+
changedRows: 0,
|
|
214
|
+
deadMemoryRows: 0,
|
|
215
|
+
deadMemoryDemotions: 0,
|
|
216
|
+
tierBackfills: 0,
|
|
217
|
+
tierChanges: 0,
|
|
218
|
+
importanceChanges: 0,
|
|
219
|
+
});
|
|
220
|
+
}
|