recallnest 1.4.1 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env +4 -0
- package/CLAUDE.md +7 -3
- package/README.md +202 -130
- package/README_CN.md +263 -190
- package/bin/recallnest.mjs +62 -0
- package/config.json +55 -0
- package/daily-recall.sh +23 -0
- package/integrations/setup-all.sh +31 -0
- package/logs/daily-recall.error.log +0 -0
- package/logs/daily-recall.log +30 -0
- package/logs/ingest-2026-04-11.log +110 -0
- package/logs/ingest-2026-04-12.log +90 -0
- package/logs/launchagent-stderr.log +0 -0
- package/logs/launchagent-stdout.log +0 -0
- package/package.json +10 -3
- package/src/access-tracker.ts +3 -2
- package/src/activity-counter.ts +109 -0
- package/src/adaptive-retrieval.ts +6 -5
- package/src/admission-control.ts +163 -0
- package/src/anchor-generator.ts +80 -0
- package/src/api-server.ts +10 -0
- package/src/audit-log.ts +122 -0
- package/src/auto-gc.ts +98 -29
- package/src/auto-recall.ts +33 -4
- package/src/capture-engine.ts +372 -23
- package/src/cascade-forget.ts +3 -2
- package/src/cli.ts +107 -5
- package/src/compat.ts +22 -0
- package/src/confidence-tracker.ts +210 -47
- package/src/conflict-store.ts +12 -3
- package/src/consolidation-engine.ts +296 -8
- package/src/context-collapse-renderer.ts +222 -0
- package/src/context-composer-stable-selection.ts +5 -1
- package/src/context-composer.ts +239 -4
- package/src/context-reconstructor.ts +404 -0
- package/src/conversation-importer.ts +441 -0
- package/src/data-checkup.ts +232 -0
- package/src/decay-engine.ts +84 -0
- package/src/distill-lock.ts +122 -0
- package/src/doctor.ts +8 -10
- package/src/dream-pipeline.ts +227 -0
- package/src/emotion-detector.ts +90 -0
- package/src/eval.ts +3 -2
- package/src/forget-engine.ts +269 -0
- package/src/frequency-tracker.ts +175 -0
- package/src/graph-export.ts +644 -0
- package/src/ingest.ts +36 -6
- package/src/interference-detector.ts +187 -0
- package/src/kg-extractor.ts +15 -24
- package/src/kg-store.ts +2 -1
- package/src/language-hook.ts +67 -0
- package/src/llm-client.ts +289 -6
- package/src/mcp-server.ts +707 -80
- package/src/memory-assets.ts +22 -5
- package/src/memory-evolution.ts +365 -0
- package/src/memory-lint.ts +429 -0
- package/src/memory-output.ts +69 -0
- package/src/memory-schema.ts +74 -2
- package/src/narrative-schema.ts +79 -0
- package/src/narrative-tagger.ts +164 -0
- package/src/pii-detector.ts +98 -0
- package/src/post-retrieval-filter.ts +76 -0
- package/src/prediction-engine.ts +253 -0
- package/src/preference-slots.ts +65 -2
- package/src/prospective-memory.ts +215 -0
- package/src/query-expander.ts +67 -0
- package/src/recall-governor.ts +183 -0
- package/src/retention-policy.ts +90 -0
- package/src/retriever.ts +828 -55
- package/src/rif.ts +34 -8
- package/src/runtime-config.ts +23 -8
- package/src/scope-inventory.ts +4 -3
- package/src/scope-policy.ts +4 -0
- package/src/session-distiller.ts +307 -0
- package/src/session-output.ts +40 -0
- package/src/session-schema.ts +37 -0
- package/src/session-store.ts +7 -2
- package/src/skill-engine.ts +208 -0
- package/src/skill-promotion.ts +293 -0
- package/src/skill-schema.ts +39 -0
- package/src/store.ts +96 -10
- package/src/topic-tag.ts +66 -0
- package/src/tracker.ts +3 -1
- package/src/ui-server.ts +80 -8
- package/src/workflow-observation-scope-review.ts +2 -1
- package/src/workflow-observation-store.ts +2 -1
- package/src/write-verifier.ts +126 -0
- package/.claude-plugin/CLAUDE.md +0 -29
- package/.claude-plugin/marketplace.json +0 -18
- package/.github/workflows/ci.yml +0 -28
- package/.mcp.json +0 -9
- package/CHANGELOG.md +0 -51
- package/DEMO.md +0 -94
- package/EVAL.md +0 -89
- package/FAILURES.md +0 -75
- package/OPERATIONS.md +0 -59
- package/ROADMAP.md +0 -198
- package/assets/recallnest-workbench.png +0 -0
- package/assets/ui/app.js +0 -807
- package/assets/ui/index.html +0 -154
- package/assets/ui/styles.css +0 -605
- package/assets/wechat-qr.jpg +0 -0
- package/daily-reflections/2026-03-05.md +0 -15
- package/daily-reflections/2026-03-06.md +0 -15
- package/daily-reflections/2026-03-07.md +0 -7
- package/daily-reflections/2026-03-08.md +0 -34
- package/daily-reflections/2026-03-09.md +0 -35
- package/daily-reflections/2026-03-10.md +0 -36
- package/daily-reflections/2026-03-11.md +0 -35
- package/daily-reflections/2026-03-12.md +0 -36
- package/daily-reflections/2026-03-13.md +0 -35
- package/daily-reflections/2026-03-14.md +0 -35
- package/daily-reflections/2026-03-15.md +0 -35
- package/daily-reflections/2026-03-16.md +0 -41
- package/daily-reflections/2026-03-17.md +0 -10
- package/daily-reflections/2026-03-18.md +0 -10
- package/daily-reflections/2026-03-19.md +0 -10
- package/daily-reflections/2026-03-20.md +0 -10
- package/daily-reflections/2026-03-21.md +0 -10
- package/daily-reflections/2026-03-22.md +0 -10
- package/daily-reflections/2026-03-23.md +0 -10
- package/daily-reflections/2026-03-24.md +0 -10
- package/daily-reflections/2026-03-25.md +0 -32
- package/daily-reflections/2026-03-26.md +0 -37
- package/daily-reflections/2026-03-27.md +0 -35
- package/daily-reflections/2026-03-28.md +0 -35
- package/daily-reflections/2026-03-29.md +0 -35
- package/daily-reflections/2026-03-30.md +0 -36
- package/daily-reflections/2026-03-31.md +0 -35
- package/daily-reflections/2026-04-01.md +0 -35
- package/docs/api-reference.md +0 -1291
- package/docs/architecture.md +0 -168
- package/docs/memory-boundary-contract.md +0 -226
- package/docs/memory-categories.md +0 -119
- package/docs/pipeline-audit-ultramemory.md +0 -177
- package/docs/plans/2026-03-08-query-expansion.md +0 -270
- package/docs/plans/2026-03-16-continuity-layer.md +0 -196
- package/docs/plans/mcp-tiering-plan.md +0 -126
- package/docs/research/recall-quality-audit-2026-03-22.md +0 -153
- package/docs/self-evolution.md +0 -198
- package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +0 -782
- package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +0 -77
- package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +0 -95
- package/eval/continuity/results-2026-03-16.md +0 -117
- package/eval/continuity/results-2026-03-19.md +0 -453
- package/eval/continuity/runbook.md +0 -110
- package/eval/continuity/seed-cases.md +0 -54
- package/eval/reports/2026-03-06-abstract-query-baseline.md +0 -96
- package/eval/reports/2026-03-06-aws-ssh-baseline.md +0 -82
- package/eval/reports/2026-03-06-baseline.md +0 -82
- package/eval/reports/query-expansion.md +0 -138
- package/lm +0 -19
- package/plan-v1.2-distributable.md +0 -105
- package/scripts/archive-distilled-junk.ts +0 -116
- package/scripts/backfill-l0l1l2.ts +0 -98
- package/scripts/blind-scan.ts +0 -588
- package/scripts/cleanup-step1-exact-dups.ts +0 -117
- package/scripts/cleanup-step2-template-dups.ts +0 -113
- package/scripts/cleanup-step3-stale-content.ts +0 -149
- package/scripts/cleanup-step4-orphan-scopes.ts +0 -131
- package/scripts/delete-distilled-junk.ts +0 -88
- package/scripts/distill-facts.ts +0 -305
- package/scripts/health-check.ts +0 -343
- package/scripts/incremental-ingest.sh +0 -71
- package/scripts/mlp-health-check.ts +0 -382
- package/scripts/pull-claude-desktop.ts +0 -515
- package/scripts/re-ingest-cc.sh +0 -66
- package/scripts/rebalance-memory-health.ts +0 -181
- package/scripts/run-desktop-ingest.sh +0 -11
- package/scripts/start-server.sh +0 -12
- package/scripts/viz-dashboard.html +0 -423
- package/scripts/viz-export.ts +0 -703
- package/src/__tests__/access-tracker-novelty.test.ts +0 -228
- package/src/__tests__/auto-consolidation.test.ts +0 -168
- package/src/__tests__/auto-recall.test.ts +0 -131
- package/src/__tests__/batch-dedup.test.ts +0 -65
- package/src/__tests__/capture-engine.test.ts +0 -1142
- package/src/__tests__/capture-heuristic.test.ts +0 -171
- package/src/__tests__/cascade-forget.test.ts +0 -210
- package/src/__tests__/category-min-scores.test.ts +0 -33
- package/src/__tests__/confidence-tracker.test.ts +0 -196
- package/src/__tests__/conflict-advisor.test.ts +0 -206
- package/src/__tests__/conflict-escalation.test.ts +0 -149
- package/src/__tests__/conflict-lifecycle.test.ts +0 -83
- package/src/__tests__/conflict-output.test.ts +0 -85
- package/src/__tests__/conflict-store.test.ts +0 -374
- package/src/__tests__/consolidation-engine.test.ts +0 -181
- package/src/__tests__/context-composer-pins.test.ts +0 -64
- package/src/__tests__/context-composer-task-selection.test.ts +0 -24
- package/src/__tests__/context-renderer.test.ts +0 -91
- package/src/__tests__/continuity-eval.test.ts +0 -275
- package/src/__tests__/continuity-schema.test.ts +0 -97
- package/src/__tests__/core-recall-eval.test.ts +0 -235
- package/src/__tests__/core-summary.test.ts +0 -109
- package/src/__tests__/dedup-actions.test.ts +0 -58
- package/src/__tests__/deterministic-id.test.ts +0 -39
- package/src/__tests__/doctor.test.ts +0 -303
- package/src/__tests__/drill-down.test.ts +0 -108
- package/src/__tests__/embedder.test.ts +0 -122
- package/src/__tests__/entity-resolver.test.ts +0 -58
- package/src/__tests__/event-segmenter.test.ts +0 -153
- package/src/__tests__/hotness-blend.test.ts +0 -76
- package/src/__tests__/ingest.test.ts +0 -360
- package/src/__tests__/kg-extractor.test.ts +0 -184
- package/src/__tests__/kg-store.test.ts +0 -198
- package/src/__tests__/llm-consolidation.test.ts +0 -226
- package/src/__tests__/memory-boundaries.test.ts +0 -237
- package/src/__tests__/memory-health-rebalance.test.ts +0 -144
- package/src/__tests__/memory-output.test.ts +0 -215
- package/src/__tests__/multi-vector.test.ts +0 -242
- package/src/__tests__/ppr-traversal.test.ts +0 -256
- package/src/__tests__/preference-guard-smoke.test.ts +0 -211
- package/src/__tests__/preference-matcher.test.ts +0 -230
- package/src/__tests__/preference-slots.test.ts +0 -106
- package/src/__tests__/prospective-memory.test.ts +0 -283
- package/src/__tests__/query-entity-detector.test.ts +0 -171
- package/src/__tests__/query-expander.test.ts +0 -34
- package/src/__tests__/recall-mode.test.ts +0 -64
- package/src/__tests__/result-synthesizer.test.ts +0 -198
- package/src/__tests__/resume-context.test.ts +0 -4743
- package/src/__tests__/retrieval-trace.test.ts +0 -213
- package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
- package/src/__tests__/retroactive-boost.test.ts +0 -223
- package/src/__tests__/scope-inventory.test.ts +0 -200
- package/src/__tests__/scope-policy.test.ts +0 -69
- package/src/__tests__/session-checkpoint.test.ts +0 -259
- package/src/__tests__/session-gc.test.ts +0 -215
- package/src/__tests__/store-scope-audit.test.ts +0 -81
- package/src/__tests__/temporal-parser.test.ts +0 -241
- package/src/__tests__/term-registry.test.ts +0 -121
- package/src/__tests__/tool-output-compressor.test.ts +0 -133
- package/src/__tests__/version-manager.test.ts +0 -213
- package/src/__tests__/workflow-observation.test.ts +0 -400
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import type { RetrievalResult } from "./retriever.js";
|
|
2
|
+
import { detectHeuristicContradiction } from "./consolidation-engine.js";
|
|
3
|
+
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Phase 4: Constructive Retrieval — Types
|
|
6
|
+
// ============================================================================
|
|
7
|
+
|
|
8
|
+
/** How a candidate entered the reconstruction pool. */
|
|
9
|
+
export type CandidateSourceType =
|
|
10
|
+
| "direct"
|
|
11
|
+
| "kg_neighbor"
|
|
12
|
+
| "evolution_chain"
|
|
13
|
+
| "cluster_member"
|
|
14
|
+
| "narrative_sibling";
|
|
15
|
+
|
|
16
|
+
export interface CandidateSource {
|
|
17
|
+
type: CandidateSourceType;
|
|
18
|
+
/** Human-readable reason, e.g. "neighbor of entity Docker" */
|
|
19
|
+
reason?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** One source entry in the reconstruction output — tracks provenance. */
|
|
23
|
+
export interface ReconstructionSourceEntry {
|
|
24
|
+
id: string;
|
|
25
|
+
source: CandidateSource;
|
|
26
|
+
/** What this memory contributed to the reconstruction (extracted from LLM output). */
|
|
27
|
+
contribution: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A detected contradiction between two source memories. */
|
|
31
|
+
export interface ContradictionEntry {
|
|
32
|
+
memoryIds: [string, string];
|
|
33
|
+
description: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// --- Reconstruction I/O ---
|
|
37
|
+
|
|
38
|
+
export interface ReconstructionInput {
|
|
39
|
+
query: string;
|
|
40
|
+
results: RetrievalResult[];
|
|
41
|
+
mode: "resume" | "search";
|
|
42
|
+
maxTokens?: number;
|
|
43
|
+
/** Phase 4: checkpoint context injected into the reconstruction prompt. */
|
|
44
|
+
checkpointContext?: {
|
|
45
|
+
openLoops?: string[];
|
|
46
|
+
nextActions?: string[];
|
|
47
|
+
scope?: string;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ReconstructionOutput {
|
|
52
|
+
reconstructed: string | null;
|
|
53
|
+
/** V2: Typed source entries with provenance tracking. */
|
|
54
|
+
sources: ReconstructionSourceEntry[];
|
|
55
|
+
confidence: number;
|
|
56
|
+
/** Detected contradictions between source memories. */
|
|
57
|
+
contradictions: ContradictionEntry[];
|
|
58
|
+
/** Source-map grounding coverage (0-1). */
|
|
59
|
+
coverage: number;
|
|
60
|
+
fallbackReason?: string;
|
|
61
|
+
raw: RetrievalResult[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface GateConditions {
|
|
65
|
+
flagEnabled: boolean;
|
|
66
|
+
callerOptIn: boolean;
|
|
67
|
+
resultCount: number;
|
|
68
|
+
llmAvailable: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ReconstructionLLMClient {
|
|
72
|
+
generateReconstruction(system: string, user: string): Promise<string | null>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ============================================================================
|
|
76
|
+
// Phase 4: Candidate Expansion
|
|
77
|
+
// ============================================================================
|
|
78
|
+
|
|
79
|
+
/** Dependency injection for candidate expansion sources. */
|
|
80
|
+
export interface CandidateExpansionDeps {
|
|
81
|
+
/** Expand via KG graph neighbor traversal. */
|
|
82
|
+
expandViaKG?: (results: RetrievalResult[]) => Promise<RetrievalResult[]>;
|
|
83
|
+
/** Expand via evolution chains (supersedes / supersededBy). */
|
|
84
|
+
expandViaEvolution?: (results: RetrievalResult[]) => Promise<RetrievalResult[]>;
|
|
85
|
+
/** Expand via cluster member lookup (sourceMemories of cluster insights). */
|
|
86
|
+
expandViaClusters?: (results: RetrievalResult[]) => Promise<RetrievalResult[]>;
|
|
87
|
+
/** Expand via narrative siblings (shared generalEventId). */
|
|
88
|
+
expandViaNarrative?: (results: RetrievalResult[]) => Promise<RetrievalResult[]>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Hard cap for total expanded candidate set. */
|
|
92
|
+
const EXPANSION_CAP = 20;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Expand direct retrieval results with related candidates from multiple sources.
|
|
96
|
+
* Returns the expanded result set + a source map for provenance tracking.
|
|
97
|
+
*/
|
|
98
|
+
export async function expandCandidates(
|
|
99
|
+
directResults: RetrievalResult[],
|
|
100
|
+
deps: CandidateExpansionDeps,
|
|
101
|
+
): Promise<{ results: RetrievalResult[]; sourceMap: Map<string, CandidateSource> }> {
|
|
102
|
+
const sourceMap = new Map<string, CandidateSource>();
|
|
103
|
+
const seen = new Set<string>();
|
|
104
|
+
const all: RetrievalResult[] = [];
|
|
105
|
+
|
|
106
|
+
// Tag direct results first
|
|
107
|
+
for (const r of directResults) {
|
|
108
|
+
sourceMap.set(r.entry.id, { type: "direct" });
|
|
109
|
+
seen.add(r.entry.id);
|
|
110
|
+
all.push(r);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (all.length >= EXPANSION_CAP) {
|
|
114
|
+
return { results: all.slice(0, EXPANSION_CAP), sourceMap };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Run all expansion sources in parallel
|
|
118
|
+
const expansionTypes: Array<{
|
|
119
|
+
type: CandidateSourceType;
|
|
120
|
+
fn?: (results: RetrievalResult[]) => Promise<RetrievalResult[]>;
|
|
121
|
+
}> = [
|
|
122
|
+
{ type: "kg_neighbor", fn: deps.expandViaKG },
|
|
123
|
+
{ type: "evolution_chain", fn: deps.expandViaEvolution },
|
|
124
|
+
{ type: "cluster_member", fn: deps.expandViaClusters },
|
|
125
|
+
{ type: "narrative_sibling", fn: deps.expandViaNarrative },
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
const expansionPromises = expansionTypes.map(({ fn }) =>
|
|
129
|
+
fn ? fn(directResults).catch(() => [] as RetrievalResult[]) : Promise.resolve([] as RetrievalResult[]),
|
|
130
|
+
);
|
|
131
|
+
const expansionResults = await Promise.all(expansionPromises);
|
|
132
|
+
|
|
133
|
+
// Merge expanded results, respecting the cap
|
|
134
|
+
for (let i = 0; i < expansionResults.length; i++) {
|
|
135
|
+
const sourceType = expansionTypes[i].type;
|
|
136
|
+
for (const r of expansionResults[i]) {
|
|
137
|
+
if (all.length >= EXPANSION_CAP) break;
|
|
138
|
+
if (seen.has(r.entry.id)) continue;
|
|
139
|
+
seen.add(r.entry.id);
|
|
140
|
+
sourceMap.set(r.entry.id, { type: sourceType });
|
|
141
|
+
all.push(r);
|
|
142
|
+
}
|
|
143
|
+
if (all.length >= EXPANSION_CAP) break;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { results: all, sourceMap };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ============================================================================
|
|
150
|
+
// Gate
|
|
151
|
+
// ============================================================================
|
|
152
|
+
|
|
153
|
+
export function shouldReconstruct(c: GateConditions): boolean {
|
|
154
|
+
return c.flagEnabled && c.callerOptIn && c.resultCount >= 3 && c.llmAvailable;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ============================================================================
|
|
158
|
+
// Grounding Utilities
|
|
159
|
+
// ============================================================================
|
|
160
|
+
|
|
161
|
+
/** Extract [src:MEMORY_ID] citations from LLM output. */
|
|
162
|
+
export function extractCitedIds(text: string): string[] {
|
|
163
|
+
const ids = new Set<string>();
|
|
164
|
+
const regex = /\[src:([^\]]+)\]/g;
|
|
165
|
+
let match: RegExpExecArray | null;
|
|
166
|
+
while ((match = regex.exec(text)) !== null) {
|
|
167
|
+
ids.add(match[1]);
|
|
168
|
+
}
|
|
169
|
+
return [...ids];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Remove sentences containing a specific citation tag. */
|
|
173
|
+
export function removeSentencesWithId(text: string, id: string): string {
|
|
174
|
+
const sentences = text.split(/(?<=[.!?])\s+/);
|
|
175
|
+
return sentences.filter(s => !s.includes(`[src:${id}]`)).join(" ");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Phase 4: Source-map grounded coverage — replaces the old lexical overlap approach.
|
|
180
|
+
* Counts what fraction of reconstruction sentences are backed by valid [src:ID] citations.
|
|
181
|
+
*/
|
|
182
|
+
export function computeSourceMapCoverage(
|
|
183
|
+
reconstructed: string,
|
|
184
|
+
validIds: Set<string>,
|
|
185
|
+
): number {
|
|
186
|
+
const sentences = reconstructed.split(/(?<=[.!?])\s+/).filter(s => s.length > 5);
|
|
187
|
+
if (sentences.length === 0) return 0;
|
|
188
|
+
|
|
189
|
+
let grounded = 0;
|
|
190
|
+
for (const sent of sentences) {
|
|
191
|
+
const cited = extractCitedIds(sent);
|
|
192
|
+
// A sentence is grounded if it has at least one valid citation
|
|
193
|
+
if (cited.some(id => validIds.has(id))) {
|
|
194
|
+
grounded++;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return grounded / sentences.length;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Legacy lexical coverage — kept for backward compat in tests.
|
|
202
|
+
* @deprecated Use computeSourceMapCoverage instead.
|
|
203
|
+
*/
|
|
204
|
+
export function computeCoverage(reconstructed: string, sourceTexts: string[]): number {
|
|
205
|
+
const sentences = reconstructed.split(/(?<=[.!?])\s+/).filter(s => s.length > 5);
|
|
206
|
+
if (sentences.length === 0) return 0;
|
|
207
|
+
|
|
208
|
+
const sourceWords = new Set<string>();
|
|
209
|
+
for (const src of sourceTexts) {
|
|
210
|
+
for (const w of src.toLowerCase().split(/\s+/)) {
|
|
211
|
+
if (w.length > 2) sourceWords.add(w);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let covered = 0;
|
|
216
|
+
for (const sent of sentences) {
|
|
217
|
+
const words = sent.toLowerCase().split(/\s+/).filter(w => w.length > 2);
|
|
218
|
+
if (words.length === 0) continue;
|
|
219
|
+
const overlap = words.filter(w => sourceWords.has(w)).length / words.length;
|
|
220
|
+
if (overlap > 0.3) covered++;
|
|
221
|
+
}
|
|
222
|
+
return covered / sentences.length;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ============================================================================
|
|
226
|
+
// Contradiction Detection
|
|
227
|
+
// ============================================================================
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Phase 4: Detect contradictions across the candidate set.
|
|
231
|
+
* Reuses the heuristic contradiction detector from consolidation-engine.
|
|
232
|
+
* O(n^2) but n is capped at EXPANSION_CAP (20), so max 190 comparisons.
|
|
233
|
+
*/
|
|
234
|
+
export function detectContradictions(results: RetrievalResult[]): ContradictionEntry[] {
|
|
235
|
+
const contradictions: ContradictionEntry[] = [];
|
|
236
|
+
const MAX_CONTRADICTIONS = 5;
|
|
237
|
+
|
|
238
|
+
for (let i = 0; i < results.length && contradictions.length < MAX_CONTRADICTIONS; i++) {
|
|
239
|
+
for (let j = i + 1; j < results.length && contradictions.length < MAX_CONTRADICTIONS; j++) {
|
|
240
|
+
if (detectHeuristicContradiction(results[i].entry.text, results[j].entry.text)) {
|
|
241
|
+
contradictions.push({
|
|
242
|
+
memoryIds: [results[i].entry.id, results[j].entry.id],
|
|
243
|
+
description: `Potential conflict between "${results[i].entry.text.slice(0, 60)}..." and "${results[j].entry.text.slice(0, 60)}..."`,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return contradictions;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ============================================================================
|
|
253
|
+
// Prompt Builder
|
|
254
|
+
// ============================================================================
|
|
255
|
+
|
|
256
|
+
export function buildPrompt(input: ReconstructionInput): { system: string; user: string } {
|
|
257
|
+
const modeHint = input.mode === "resume"
|
|
258
|
+
? "Focus on what the user was working on, key decisions, and pending actions."
|
|
259
|
+
: "Focus on the most relevant facts for the query.";
|
|
260
|
+
|
|
261
|
+
// Phase 4: Inject checkpoint context into the prompt for richer reconstruction
|
|
262
|
+
let checkpointHint = "";
|
|
263
|
+
if (input.checkpointContext) {
|
|
264
|
+
const parts: string[] = [];
|
|
265
|
+
if (input.checkpointContext.scope) {
|
|
266
|
+
parts.push(`Active scope: ${input.checkpointContext.scope}`);
|
|
267
|
+
}
|
|
268
|
+
if (input.checkpointContext.openLoops?.length) {
|
|
269
|
+
parts.push(`Open loops:\n${input.checkpointContext.openLoops.map(l => `- ${l}`).join("\n")}`);
|
|
270
|
+
}
|
|
271
|
+
if (input.checkpointContext.nextActions?.length) {
|
|
272
|
+
parts.push(`Next actions:\n${input.checkpointContext.nextActions.map(a => `- ${a}`).join("\n")}`);
|
|
273
|
+
}
|
|
274
|
+
if (parts.length > 0) {
|
|
275
|
+
checkpointHint = `\n\nCheckpoint state:\n${parts.join("\n")}`;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const system = `You are a memory reconstruction engine. Synthesize a coherent summary from stored memories.\n\n${modeHint}\n\nRules:\n1. Every claim MUST cite [src:MEMORY_ID]\n2. Do NOT invent facts not in source memories\n3. Contradictions: present both sides with [conflict] tag and cite both sources\n4. Keep under ${input.maxTokens ?? 500} tokens${checkpointHint}`;
|
|
280
|
+
|
|
281
|
+
const block = input.results.slice(0, 10).map(r =>
|
|
282
|
+
`[ID: ${r.entry.id}] ${r.entry.text} (importance: ${r.entry.importance})`
|
|
283
|
+
).join("\n\n");
|
|
284
|
+
|
|
285
|
+
return { system, user: `Context: ${input.query}\n\nMemories:\n${block}\n\nReconstruct:` };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ============================================================================
|
|
289
|
+
// Main Pipeline
|
|
290
|
+
// ============================================================================
|
|
291
|
+
|
|
292
|
+
const TIMEOUT_MS = 3000;
|
|
293
|
+
const COVERAGE_FLOOR = 0.5;
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Phase 4 Constructive Retrieval pipeline.
|
|
297
|
+
*
|
|
298
|
+
* 1. Expand candidates (if deps provided)
|
|
299
|
+
* 2. Detect contradictions across expanded set
|
|
300
|
+
* 3. Run LLM reconstruction with grounded prompt
|
|
301
|
+
* 4. Verify via source-map coverage (not lexical overlap)
|
|
302
|
+
* 5. Return typed ReconstructionOutput with sources, contradictions, coverage
|
|
303
|
+
*/
|
|
304
|
+
export async function reconstruct(
|
|
305
|
+
input: ReconstructionInput,
|
|
306
|
+
llmClient: ReconstructionLLMClient,
|
|
307
|
+
expansionDeps?: CandidateExpansionDeps,
|
|
308
|
+
): Promise<ReconstructionOutput> {
|
|
309
|
+
const emptyOutput = (reason: string, raw: RetrievalResult[]): ReconstructionOutput => ({
|
|
310
|
+
reconstructed: null,
|
|
311
|
+
sources: [],
|
|
312
|
+
confidence: 0,
|
|
313
|
+
contradictions: [],
|
|
314
|
+
coverage: 0,
|
|
315
|
+
fallbackReason: reason,
|
|
316
|
+
raw,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Step 1: Candidate expansion (if deps provided)
|
|
320
|
+
let expandedResults = input.results;
|
|
321
|
+
let sourceMap = new Map<string, CandidateSource>(
|
|
322
|
+
input.results.map(r => [r.entry.id, { type: "direct" as CandidateSourceType }]),
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
if (expansionDeps) {
|
|
326
|
+
const expanded = await expandCandidates(input.results, expansionDeps);
|
|
327
|
+
expandedResults = expanded.results;
|
|
328
|
+
sourceMap = expanded.sourceMap;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Step 2: Detect contradictions across the candidate set
|
|
332
|
+
const contradictions = detectContradictions(expandedResults);
|
|
333
|
+
|
|
334
|
+
// Build input with expanded results
|
|
335
|
+
const expandedInput: ReconstructionInput = { ...input, results: expandedResults };
|
|
336
|
+
const raw = expandedResults;
|
|
337
|
+
|
|
338
|
+
const timeout = new Promise<ReconstructionOutput>(resolve =>
|
|
339
|
+
setTimeout(() => resolve(emptyOutput("timeout", raw)), TIMEOUT_MS),
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
const work = (async (): Promise<ReconstructionOutput> => {
|
|
343
|
+
const { system, user } = buildPrompt(expandedInput);
|
|
344
|
+
const response = await llmClient.generateReconstruction(system, user);
|
|
345
|
+
|
|
346
|
+
if (!response) {
|
|
347
|
+
return emptyOutput("llm_empty", raw);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
let text = response;
|
|
351
|
+
let confidence = 1.0;
|
|
352
|
+
|
|
353
|
+
// Layer 1: ID verification — remove sentences citing non-existent memory IDs
|
|
354
|
+
const validIds = new Set(raw.map(r => r.entry.id));
|
|
355
|
+
for (const id of extractCitedIds(text)) {
|
|
356
|
+
if (!validIds.has(id)) {
|
|
357
|
+
text = removeSentencesWithId(text, id);
|
|
358
|
+
confidence -= 0.2;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
confidence = Math.max(0, confidence);
|
|
362
|
+
|
|
363
|
+
// Layer 2: Source-map grounded coverage (replaces lexical overlap)
|
|
364
|
+
const coverage = computeSourceMapCoverage(text, validIds);
|
|
365
|
+
if (coverage < COVERAGE_FLOOR) {
|
|
366
|
+
return { ...emptyOutput("low_grounding", raw), contradictions, coverage };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Build typed source entries from cited IDs + source map
|
|
370
|
+
const citedIds = extractCitedIds(text).filter(id => validIds.has(id));
|
|
371
|
+
const sources: ReconstructionSourceEntry[] = citedIds.map(id => ({
|
|
372
|
+
id,
|
|
373
|
+
source: sourceMap.get(id) ?? { type: "direct" as CandidateSourceType },
|
|
374
|
+
contribution: extractContributionForId(text, id),
|
|
375
|
+
}));
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
reconstructed: text,
|
|
379
|
+
sources,
|
|
380
|
+
confidence: Math.min(confidence, coverage),
|
|
381
|
+
contradictions,
|
|
382
|
+
coverage,
|
|
383
|
+
raw,
|
|
384
|
+
};
|
|
385
|
+
})();
|
|
386
|
+
|
|
387
|
+
return Promise.race([work, timeout]);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ============================================================================
|
|
391
|
+
// Helpers
|
|
392
|
+
// ============================================================================
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Extract the sentence(s) citing a specific memory ID as its "contribution".
|
|
396
|
+
* Returns the first sentence that cites the ID, truncated.
|
|
397
|
+
*/
|
|
398
|
+
function extractContributionForId(text: string, id: string): string {
|
|
399
|
+
const sentences = text.split(/(?<=[.!?])\s+/);
|
|
400
|
+
const match = sentences.find(s => s.includes(`[src:${id}]`));
|
|
401
|
+
if (!match) return "";
|
|
402
|
+
// Strip the citation tag and truncate
|
|
403
|
+
return match.replace(/\[src:[^\]]+\]/g, "").trim().slice(0, 120);
|
|
404
|
+
}
|