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
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
|
|
20
20
|
import type { MemoryEntry, MemorySearchResult, MemoryStore } from "./store.js";
|
|
21
21
|
import { createVersionGroup } from "./version-manager.js";
|
|
22
|
+
import { isActiveMemory, parseEvolution, buildSupersedeMetadata, buildConsolidatedMetadata, patchEvolution } from "./memory-evolution.js";
|
|
23
|
+
import { cosineSimilarity } from "./multi-vector.js";
|
|
24
|
+
import type { LLMClient } from "./llm-client.js";
|
|
25
|
+
import type { Embedder } from "./embedder.js";
|
|
22
26
|
|
|
23
27
|
// ---------------------------------------------------------------------------
|
|
24
28
|
// Config & Types
|
|
@@ -64,18 +68,17 @@ function parseMetadata(entry: MemoryEntry): Record<string, unknown> {
|
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
function isActive(entry: MemoryEntry): boolean {
|
|
67
|
-
|
|
68
|
-
return
|
|
71
|
+
// Unified lifecycle check via evolution.status (no legacy meta.state fallback)
|
|
72
|
+
return isActiveMemory(entry.metadata);
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
function canonicalScore(entry: MemoryEntry): number {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
return entry.importance * (1 + Math.log(accessCount + 1));
|
|
76
|
+
const evo = parseEvolution(entry.metadata, entry.timestamp);
|
|
77
|
+
return entry.importance * (1 + Math.log(evo.accessCount + 1));
|
|
75
78
|
}
|
|
76
79
|
|
|
77
80
|
/** Simple heuristic contradiction: negation pattern check between two texts. */
|
|
78
|
-
function detectHeuristicContradiction(textA: string, textB: string): boolean {
|
|
81
|
+
export function detectHeuristicContradiction(textA: string, textB: string): boolean {
|
|
79
82
|
const a = textA.toLowerCase();
|
|
80
83
|
const b = textB.toLowerCase();
|
|
81
84
|
|
|
@@ -197,9 +200,19 @@ export class ConsolidationEngine {
|
|
|
197
200
|
const sim = scoreMap.get(member.id) ?? clusterThreshold;
|
|
198
201
|
|
|
199
202
|
if (sim >= mergeThreshold) {
|
|
200
|
-
// Tier 3.3: Version coexistence
|
|
201
|
-
// Both entries stay active but are grouped — retrieval picks the best.
|
|
203
|
+
// Tier 3.3: Version coexistence — both stay but grouped.
|
|
202
204
|
await createVersionGroup(this.store, canonical, member, scope);
|
|
205
|
+
// C-1: Also mark the weaker entry as consolidated via evolution metadata
|
|
206
|
+
const consolidatedMeta = buildConsolidatedMetadata(member.metadata, canonical.id);
|
|
207
|
+
await this.store.update(member.id, { metadata: consolidatedMeta }, [scope]);
|
|
208
|
+
// Mark canonical as having source memories
|
|
209
|
+
const canonEvo = parseEvolution(canonical.metadata, canonical.timestamp);
|
|
210
|
+
if (!canonEvo.sourceMemories.includes(member.id)) {
|
|
211
|
+
const updatedCanon = patchEvolution(canonical.metadata, {
|
|
212
|
+
sourceMemories: [...canonEvo.sourceMemories, member.id],
|
|
213
|
+
});
|
|
214
|
+
await this.store.update(canonical.id, { metadata: updatedCanon }, [scope]);
|
|
215
|
+
}
|
|
203
216
|
mergedCount++;
|
|
204
217
|
} else {
|
|
205
218
|
// Link: add clustered_with relation
|
|
@@ -236,6 +249,243 @@ export class ConsolidationEngine {
|
|
|
236
249
|
}
|
|
237
250
|
}
|
|
238
251
|
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
253
|
+
// C-2: Cluster Consolidation — group similar memories and generate insights
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
|
|
256
|
+
export interface ClusterConsolidationResult {
|
|
257
|
+
clustersFound: number;
|
|
258
|
+
clustersConsolidated: number;
|
|
259
|
+
insightsGenerated: number;
|
|
260
|
+
/** HP-5: Number of cross-memory patterns discovered */
|
|
261
|
+
patternsExtracted: number;
|
|
262
|
+
entriesLinked: number;
|
|
263
|
+
/** CC-9: Set when consecutive low-yield rounds trigger early termination */
|
|
264
|
+
earlyStop?: "diminishing_returns";
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* C-2: Cluster consolidation — group similar memories and generate insights.
|
|
269
|
+
*
|
|
270
|
+
* Algorithm:
|
|
271
|
+
* 1. Take active memories in a scope
|
|
272
|
+
* 2. Cluster by embedding similarity (simple greedy clustering, not full K-means)
|
|
273
|
+
* 3. For clusters with > minClusterSize members, generate a high-level insight via LLM
|
|
274
|
+
* 4. Store insight as new memory, link source memories via evolution sourceMemories field
|
|
275
|
+
* 5. Source memories marked consolidated_into but remain active (still individually searchable)
|
|
276
|
+
*/
|
|
277
|
+
export async function clusterAndConsolidate(params: {
|
|
278
|
+
entries: MemoryEntry[];
|
|
279
|
+
embedder: Pick<Embedder, "embedPassage">;
|
|
280
|
+
llm: LLMClient;
|
|
281
|
+
store: Pick<MemoryStore, "store" | "update">;
|
|
282
|
+
scope: string;
|
|
283
|
+
/** Minimum cluster size to trigger consolidation (default: 3) */
|
|
284
|
+
minClusterSize?: number;
|
|
285
|
+
/** Similarity threshold for clustering (default: 0.75) */
|
|
286
|
+
clusterThreshold?: number;
|
|
287
|
+
/** Max clusters to process per run (default: 5) */
|
|
288
|
+
maxClusters?: number;
|
|
289
|
+
/** HP-5: Enable cross-memory pattern extraction after insight generation (default: false) */
|
|
290
|
+
extractPatterns?: boolean;
|
|
291
|
+
}): Promise<ClusterConsolidationResult> {
|
|
292
|
+
const {
|
|
293
|
+
entries,
|
|
294
|
+
llm,
|
|
295
|
+
store,
|
|
296
|
+
scope,
|
|
297
|
+
minClusterSize = 3,
|
|
298
|
+
clusterThreshold = 0.75,
|
|
299
|
+
maxClusters = 5,
|
|
300
|
+
extractPatterns = false,
|
|
301
|
+
} = params;
|
|
302
|
+
|
|
303
|
+
const result: ClusterConsolidationResult = {
|
|
304
|
+
clustersFound: 0,
|
|
305
|
+
clustersConsolidated: 0,
|
|
306
|
+
insightsGenerated: 0,
|
|
307
|
+
patternsExtracted: 0,
|
|
308
|
+
entriesLinked: 0,
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
// Filter to active entries with vectors
|
|
312
|
+
const active = entries.filter(e => isActiveMemory(e.metadata) && e.vector?.length > 0);
|
|
313
|
+
if (active.length === 0) return result;
|
|
314
|
+
|
|
315
|
+
// Step 1: Greedy clustering by embedding similarity
|
|
316
|
+
const clusters: MemoryEntry[][] = [];
|
|
317
|
+
const centroids: number[][] = [];
|
|
318
|
+
const assigned = new Set<string>();
|
|
319
|
+
|
|
320
|
+
for (const entry of active) {
|
|
321
|
+
if (assigned.has(entry.id)) continue;
|
|
322
|
+
|
|
323
|
+
let bestClusterIdx = -1;
|
|
324
|
+
let bestSim = -1;
|
|
325
|
+
|
|
326
|
+
for (let ci = 0; ci < centroids.length; ci++) {
|
|
327
|
+
const sim = cosineSimilarity(entry.vector, centroids[ci]);
|
|
328
|
+
if (sim > clusterThreshold && sim > bestSim) {
|
|
329
|
+
bestSim = sim;
|
|
330
|
+
bestClusterIdx = ci;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (bestClusterIdx >= 0) {
|
|
335
|
+
clusters[bestClusterIdx].push(entry);
|
|
336
|
+
assigned.add(entry.id);
|
|
337
|
+
// Update centroid as running average
|
|
338
|
+
const members = clusters[bestClusterIdx];
|
|
339
|
+
const dim = centroids[bestClusterIdx].length;
|
|
340
|
+
const newCentroid = new Array<number>(dim);
|
|
341
|
+
for (let d = 0; d < dim; d++) {
|
|
342
|
+
let sum = 0;
|
|
343
|
+
for (const m of members) sum += m.vector[d];
|
|
344
|
+
newCentroid[d] = sum / members.length;
|
|
345
|
+
}
|
|
346
|
+
centroids[bestClusterIdx] = newCentroid;
|
|
347
|
+
} else {
|
|
348
|
+
// Start a new cluster
|
|
349
|
+
clusters.push([entry]);
|
|
350
|
+
centroids.push([...entry.vector]);
|
|
351
|
+
assigned.add(entry.id);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Step 2: Filter to clusters meeting minClusterSize
|
|
356
|
+
const qualifiedClusters = clusters.filter(c => c.length >= minClusterSize);
|
|
357
|
+
result.clustersFound = qualifiedClusters.length;
|
|
358
|
+
|
|
359
|
+
if (qualifiedClusters.length === 0) return result;
|
|
360
|
+
|
|
361
|
+
// Step 3: Process up to maxClusters, with CC-9 diminishing returns detection
|
|
362
|
+
const toProcess = qualifiedClusters.slice(0, maxClusters);
|
|
363
|
+
let consecutiveLowYield = 0;
|
|
364
|
+
|
|
365
|
+
for (const cluster of toProcess) {
|
|
366
|
+
// CC-9: Check diminishing returns — 2 consecutive rounds with <= 1 insight
|
|
367
|
+
if (consecutiveLowYield >= 2) {
|
|
368
|
+
result.earlyStop = "diminishing_returns";
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Generate insight via LLM from cluster member texts
|
|
373
|
+
const combinedText = cluster.map(m => m.text).join("\n---\n");
|
|
374
|
+
const insight = await llm.generateL0(combinedText);
|
|
375
|
+
|
|
376
|
+
if (!insight) {
|
|
377
|
+
// CC-9: No insight produced — this is a zero-yield round
|
|
378
|
+
consecutiveLowYield++;
|
|
379
|
+
result.clustersConsolidated++;
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Determine category (majority vote from cluster members)
|
|
384
|
+
const catCounts = new Map<string, number>();
|
|
385
|
+
for (const m of cluster) {
|
|
386
|
+
catCounts.set(m.category, (catCounts.get(m.category) ?? 0) + 1);
|
|
387
|
+
}
|
|
388
|
+
let bestCat = cluster[0].category;
|
|
389
|
+
let bestCount = 0;
|
|
390
|
+
for (const [cat, count] of catCounts) {
|
|
391
|
+
if (count > bestCount) {
|
|
392
|
+
bestCat = cat as MemoryEntry["category"];
|
|
393
|
+
bestCount = count;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Importance = max of cluster members
|
|
398
|
+
const maxImportance = Math.max(...cluster.map(m => m.importance));
|
|
399
|
+
|
|
400
|
+
// Embed the insight text
|
|
401
|
+
const insightVector = await params.embedder.embedPassage(insight);
|
|
402
|
+
|
|
403
|
+
// Store the insight as a new memory
|
|
404
|
+
const insightEntry = await store.store({
|
|
405
|
+
text: insight,
|
|
406
|
+
vector: insightVector,
|
|
407
|
+
category: bestCat,
|
|
408
|
+
scope,
|
|
409
|
+
importance: maxImportance,
|
|
410
|
+
metadata: JSON.stringify({
|
|
411
|
+
evolution: {
|
|
412
|
+
status: "active",
|
|
413
|
+
version: 1,
|
|
414
|
+
accessCount: 0,
|
|
415
|
+
lastAccessedAt: null,
|
|
416
|
+
supersededBy: null,
|
|
417
|
+
consolidatedInto: null,
|
|
418
|
+
sourceMemories: cluster.map(m => m.id),
|
|
419
|
+
validFrom: Date.now(),
|
|
420
|
+
validUntil: null,
|
|
421
|
+
},
|
|
422
|
+
cluster_insight: true,
|
|
423
|
+
}),
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
result.insightsGenerated++;
|
|
427
|
+
|
|
428
|
+
// Mark source memories as consolidated_into (but keep them active)
|
|
429
|
+
for (const member of cluster) {
|
|
430
|
+
const patched = patchEvolution(member.metadata, {
|
|
431
|
+
consolidatedInto: insightEntry.id,
|
|
432
|
+
// Keep status active — still individually searchable
|
|
433
|
+
});
|
|
434
|
+
await store.update(member.id, { metadata: patched }, [scope]);
|
|
435
|
+
result.entriesLinked++;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
result.clustersConsolidated++;
|
|
439
|
+
|
|
440
|
+
// HP-5: Cross-memory pattern extraction
|
|
441
|
+
if (extractPatterns && cluster.length >= 3) {
|
|
442
|
+
const patternText = await llm.extractPattern(cluster.map(m => m.text));
|
|
443
|
+
if (patternText) {
|
|
444
|
+
const patternImportance = Math.min(maxImportance + 0.1, 1.0);
|
|
445
|
+
const patternVector = await params.embedder.embedPassage(patternText);
|
|
446
|
+
const patternEntry = await store.store({
|
|
447
|
+
text: patternText,
|
|
448
|
+
vector: patternVector,
|
|
449
|
+
category: "patterns",
|
|
450
|
+
scope,
|
|
451
|
+
importance: patternImportance,
|
|
452
|
+
metadata: JSON.stringify({
|
|
453
|
+
evolution: {
|
|
454
|
+
status: "active",
|
|
455
|
+
version: 1,
|
|
456
|
+
accessCount: 0,
|
|
457
|
+
lastAccessedAt: null,
|
|
458
|
+
supersededBy: null,
|
|
459
|
+
consolidatedInto: null,
|
|
460
|
+
contributedToPattern: null,
|
|
461
|
+
sourceMemories: cluster.map(m => m.id),
|
|
462
|
+
validFrom: Date.now(),
|
|
463
|
+
validUntil: null,
|
|
464
|
+
},
|
|
465
|
+
cross_memory_pattern: true,
|
|
466
|
+
source_cluster_size: cluster.length,
|
|
467
|
+
}),
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// Mark source memories with contributedToPattern
|
|
471
|
+
for (const member of cluster) {
|
|
472
|
+
const patched = patchEvolution(member.metadata, {
|
|
473
|
+
contributedToPattern: patternEntry.id,
|
|
474
|
+
});
|
|
475
|
+
await store.update(member.id, { metadata: patched }, [scope]);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
result.patternsExtracted++;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// CC-9: Successful insight generated — reset diminishing returns counter
|
|
483
|
+
consecutiveLowYield = 0;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return result;
|
|
487
|
+
}
|
|
488
|
+
|
|
239
489
|
/** Format a ConsolidationResult for display. */
|
|
240
490
|
export function formatConsolidationResult(result: ConsolidationResult): string {
|
|
241
491
|
const lines = [
|
|
@@ -254,3 +504,41 @@ export function formatConsolidationResult(result: ConsolidationResult): string {
|
|
|
254
504
|
}
|
|
255
505
|
return lines.join("\n");
|
|
256
506
|
}
|
|
507
|
+
|
|
508
|
+
// ---------------------------------------------------------------------------
|
|
509
|
+
// LC-P2: Cluster-aware deduplication for retrieval results
|
|
510
|
+
// ---------------------------------------------------------------------------
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* LC-P2: When a cluster insight and its source memories both appear in
|
|
514
|
+
* retrieval results, keep only the cluster insight (it subsumes the
|
|
515
|
+
* individual entries). This saves token budget during context injection.
|
|
516
|
+
*
|
|
517
|
+
* Works with any result shape that has { entry: MemoryEntry; score: number }.
|
|
518
|
+
*/
|
|
519
|
+
export function deduplicateByClusterInsight<T extends { entry: MemoryEntry; score: number }>(
|
|
520
|
+
results: T[],
|
|
521
|
+
): T[] {
|
|
522
|
+
// Collect source memory IDs covered by cluster insights in this result set
|
|
523
|
+
const coveredByInsight = new Set<string>();
|
|
524
|
+
|
|
525
|
+
for (const r of results) {
|
|
526
|
+
if (!r.entry.metadata) continue;
|
|
527
|
+
try {
|
|
528
|
+
const meta = JSON.parse(r.entry.metadata);
|
|
529
|
+
if (meta.cluster_insight === true || meta.cross_memory_pattern === true) {
|
|
530
|
+
const sources: unknown[] = meta.evolution?.sourceMemories;
|
|
531
|
+
if (Array.isArray(sources)) {
|
|
532
|
+
for (const id of sources) {
|
|
533
|
+
if (typeof id === "string") coveredByInsight.add(id);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
} catch { /* skip unparseable */ }
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (coveredByInsight.size === 0) return results;
|
|
541
|
+
|
|
542
|
+
// Filter out source memories that are subsumed by an insight
|
|
543
|
+
return results.filter(r => !coveredByInsight.has(r.entry.id));
|
|
544
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CC-7: Context Collapse — dynamic granularity rendering layer.
|
|
3
|
+
*
|
|
4
|
+
* Given retrieval results + scores, renders each item at L0/L1/L2 granularity
|
|
5
|
+
* based on relevance score. High-relevance items get full content (L2),
|
|
6
|
+
* medium gets overview (L1), low gets one-liner (L0).
|
|
7
|
+
*
|
|
8
|
+
* Also includes CC-2 staleness hints: items older than 7 days get an age warning
|
|
9
|
+
* so the model can judge whether to trust the information.
|
|
10
|
+
*
|
|
11
|
+
* Reuses existing L0/L1/L2 text stored in metadata by the multi-vector pipeline.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Types
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
export type RenderLevel = "L0" | "L1" | "L2";
|
|
19
|
+
|
|
20
|
+
export interface CollapseInput {
|
|
21
|
+
entryId: string;
|
|
22
|
+
text: string;
|
|
23
|
+
metadata?: string;
|
|
24
|
+
score: number;
|
|
25
|
+
timestamp: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CollapseOutput {
|
|
29
|
+
entryId: string;
|
|
30
|
+
text: string;
|
|
31
|
+
renderLevel: RenderLevel;
|
|
32
|
+
stalenessHint?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CollapseConfig {
|
|
36
|
+
/** Total token budget for all rendered items (default: 8000) */
|
|
37
|
+
tokenBudget: number;
|
|
38
|
+
/** Score thresholds for each level */
|
|
39
|
+
thresholds: { l2: number; l1: number; l0: number };
|
|
40
|
+
/** Days before staleness hint is added (default: 7) */
|
|
41
|
+
stalenessThresholdDays: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Defaults
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
export const DEFAULT_COLLAPSE_CONFIG: CollapseConfig = {
|
|
49
|
+
tokenBudget: 8000,
|
|
50
|
+
thresholds: { l2: 0.85, l1: 0.65, l0: 0.50 },
|
|
51
|
+
stalenessThresholdDays: 7,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Text extraction at each level
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
function parseMetaJson(metadata?: string): Record<string, unknown> | null {
|
|
59
|
+
if (!metadata) return null;
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(metadata);
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Extract L0 text (one-liner, ≤80 chars). Falls back to first sentence of full text. */
|
|
68
|
+
function extractL0(text: string, metadata?: string): string {
|
|
69
|
+
const meta = parseMetaJson(metadata);
|
|
70
|
+
if (meta) {
|
|
71
|
+
if (typeof meta.l0_abstract === "string" && meta.l0_abstract.length > 5) return meta.l0_abstract;
|
|
72
|
+
if (typeof meta.anchor === "string" && meta.anchor.length > 5) return meta.anchor;
|
|
73
|
+
}
|
|
74
|
+
// Fallback: first sentence or first 80 chars
|
|
75
|
+
const firstSentence = text.match(/^[^.!?。!?\n]+[.!?。!?]?/)?.[0];
|
|
76
|
+
if (firstSentence && firstSentence.length <= 120) return firstSentence;
|
|
77
|
+
return text.slice(0, 80) + (text.length > 80 ? "…" : "");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Extract L1 text (structured overview, 2-5 lines). Falls back to L0. */
|
|
81
|
+
function extractL1(text: string, metadata?: string): string {
|
|
82
|
+
const meta = parseMetaJson(metadata);
|
|
83
|
+
if (meta) {
|
|
84
|
+
if (typeof meta.core_summary === "string" && meta.core_summary.length > 5) return meta.core_summary;
|
|
85
|
+
if (typeof meta.l1_overview === "string" && meta.l1_overview.length > 5) return meta.l1_overview;
|
|
86
|
+
}
|
|
87
|
+
// Fallback: truncated full text
|
|
88
|
+
if (text.length <= 300) return text;
|
|
89
|
+
return text.slice(0, 300) + "…";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Extract L2 text (full content). */
|
|
93
|
+
function extractL2(text: string): string {
|
|
94
|
+
return text;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Token estimation
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
/** Rough token estimate: CJK chars ≈ 1.5 tokens each, others ≈ 0.25 per char. */
|
|
102
|
+
function estimateTokens(text: string): number {
|
|
103
|
+
let tokens = 0;
|
|
104
|
+
for (const char of text) {
|
|
105
|
+
tokens += /[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff]/.test(char) ? 1.5 : 0.25;
|
|
106
|
+
}
|
|
107
|
+
return Math.ceil(tokens);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Staleness hint (CC-2)
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
function buildStalenessHint(timestamp: number, thresholdDays: number, now?: number): string | undefined {
|
|
115
|
+
const ts = now ?? Date.now();
|
|
116
|
+
const daysSinceUpdate = Math.floor((ts - timestamp) / 86_400_000);
|
|
117
|
+
if (daysSinceUpdate <= thresholdDays) return undefined;
|
|
118
|
+
return `[Updated ${daysSinceUpdate} days ago — verify before acting on this]`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Core: collapse renderer
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Render retrieval results at adaptive granularity based on relevance score.
|
|
127
|
+
*
|
|
128
|
+
* - score >= l2 threshold → full content (L2)
|
|
129
|
+
* - score >= l1 threshold → overview (L1)
|
|
130
|
+
* - score >= l0 threshold → one-liner (L0)
|
|
131
|
+
* - below l0 threshold → excluded
|
|
132
|
+
*
|
|
133
|
+
* Token budget is enforced: once budget is exhausted, remaining items
|
|
134
|
+
* are downgraded to L0 or excluded.
|
|
135
|
+
*/
|
|
136
|
+
export function collapseResults(
|
|
137
|
+
items: CollapseInput[],
|
|
138
|
+
config?: Partial<CollapseConfig>,
|
|
139
|
+
): CollapseOutput[] {
|
|
140
|
+
const cfg: CollapseConfig = { ...DEFAULT_COLLAPSE_CONFIG, ...config };
|
|
141
|
+
const { thresholds, tokenBudget, stalenessThresholdDays } = cfg;
|
|
142
|
+
|
|
143
|
+
// Sort by score descending (highest relevance first)
|
|
144
|
+
const sorted = [...items].sort((a, b) => b.score - a.score);
|
|
145
|
+
|
|
146
|
+
const output: CollapseOutput[] = [];
|
|
147
|
+
let tokensUsed = 0;
|
|
148
|
+
|
|
149
|
+
for (const item of sorted) {
|
|
150
|
+
// Below minimum threshold → skip
|
|
151
|
+
if (item.score < thresholds.l0) continue;
|
|
152
|
+
|
|
153
|
+
// Determine target level based on score
|
|
154
|
+
let targetLevel: RenderLevel;
|
|
155
|
+
if (item.score >= thresholds.l2) {
|
|
156
|
+
targetLevel = "L2";
|
|
157
|
+
} else if (item.score >= thresholds.l1) {
|
|
158
|
+
targetLevel = "L1";
|
|
159
|
+
} else {
|
|
160
|
+
targetLevel = "L0";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Extract text at target level
|
|
164
|
+
let text: string;
|
|
165
|
+
let actualLevel: RenderLevel = targetLevel;
|
|
166
|
+
|
|
167
|
+
if (targetLevel === "L2") {
|
|
168
|
+
text = extractL2(item.text);
|
|
169
|
+
} else if (targetLevel === "L1") {
|
|
170
|
+
text = extractL1(item.text, item.metadata);
|
|
171
|
+
} else {
|
|
172
|
+
text = extractL0(item.text, item.metadata);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Check token budget — downgrade if needed
|
|
176
|
+
const tokens = estimateTokens(text);
|
|
177
|
+
if (tokensUsed + tokens > tokenBudget) {
|
|
178
|
+
// Try downgrading to L1
|
|
179
|
+
if (actualLevel === "L2") {
|
|
180
|
+
text = extractL1(item.text, item.metadata);
|
|
181
|
+
actualLevel = "L1";
|
|
182
|
+
const l1Tokens = estimateTokens(text);
|
|
183
|
+
if (tokensUsed + l1Tokens > tokenBudget) {
|
|
184
|
+
// Further downgrade to L0
|
|
185
|
+
text = extractL0(item.text, item.metadata);
|
|
186
|
+
actualLevel = "L0";
|
|
187
|
+
const l0Tokens = estimateTokens(text);
|
|
188
|
+
if (tokensUsed + l0Tokens > tokenBudget) continue; // Skip entirely
|
|
189
|
+
tokensUsed += l0Tokens;
|
|
190
|
+
} else {
|
|
191
|
+
tokensUsed += l1Tokens;
|
|
192
|
+
}
|
|
193
|
+
} else if (actualLevel === "L1") {
|
|
194
|
+
text = extractL0(item.text, item.metadata);
|
|
195
|
+
actualLevel = "L0";
|
|
196
|
+
const l0Tokens = estimateTokens(text);
|
|
197
|
+
if (tokensUsed + l0Tokens > tokenBudget) continue;
|
|
198
|
+
tokensUsed += l0Tokens;
|
|
199
|
+
} else {
|
|
200
|
+
// Already L0 and still over budget → skip
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
tokensUsed += tokens;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// CC-2: Staleness hint
|
|
208
|
+
const stalenessHint = buildStalenessHint(item.timestamp, stalenessThresholdDays);
|
|
209
|
+
|
|
210
|
+
output.push({
|
|
211
|
+
entryId: item.entryId,
|
|
212
|
+
text,
|
|
213
|
+
renderLevel: actualLevel,
|
|
214
|
+
...(stalenessHint ? { stalenessHint } : {}),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return output;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Re-export helpers for testing
|
|
222
|
+
export { extractL0, extractL1, extractL2, estimateTokens, buildStalenessHint };
|
|
@@ -2,6 +2,7 @@ import { extractBoundaryMetadata, extractCanonicalKey, shouldUseStableMemoryResu
|
|
|
2
2
|
import { buildProjectScopeCueTerms, normalizeScopedValue } from "./context-composer-scope.js";
|
|
3
3
|
import { bestSummaryText, cleanText, dedupeText, stripConversationMarkers } from "./context-composer-text.js";
|
|
4
4
|
import type { RetrievalResult } from "./retriever.js";
|
|
5
|
+
import { getConfidence } from "./confidence-tracker.js";
|
|
5
6
|
import {
|
|
6
7
|
GENERIC_ENTITY_TASK_TERMS,
|
|
7
8
|
GENERIC_SCOPE_TERMS,
|
|
@@ -158,7 +159,10 @@ function hasUnsupportedPreferenceSpecificity(result: RetrievalResult, taskSeed?:
|
|
|
158
159
|
|
|
159
160
|
function formatStableResult(category: StableCategory, result: RetrievalResult): string {
|
|
160
161
|
const text = bestSummaryText(result.entry.text, result.entry.metadata);
|
|
161
|
-
|
|
162
|
+
// F1: Tag low-confidence memories in resume_context output
|
|
163
|
+
const conf = getConfidence(result.entry);
|
|
164
|
+
const tag = conf < 0.5 ? " [低置信]" : "";
|
|
165
|
+
return cleanText(`${STABLE_CATEGORY_LABELS[category]}: ${text}${tag}`, 230);
|
|
162
166
|
}
|
|
163
167
|
|
|
164
168
|
export function selectStableResults(
|