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.
Files changed (233) hide show
  1. package/.env +4 -0
  2. package/CLAUDE.md +7 -3
  3. package/README.md +202 -130
  4. package/README_CN.md +263 -190
  5. package/bin/recallnest.mjs +62 -0
  6. package/config.json +55 -0
  7. package/daily-recall.sh +23 -0
  8. package/integrations/setup-all.sh +31 -0
  9. package/logs/daily-recall.error.log +0 -0
  10. package/logs/daily-recall.log +30 -0
  11. package/logs/ingest-2026-04-11.log +110 -0
  12. package/logs/ingest-2026-04-12.log +90 -0
  13. package/logs/launchagent-stderr.log +0 -0
  14. package/logs/launchagent-stdout.log +0 -0
  15. package/package.json +10 -3
  16. package/src/access-tracker.ts +3 -2
  17. package/src/activity-counter.ts +109 -0
  18. package/src/adaptive-retrieval.ts +6 -5
  19. package/src/admission-control.ts +163 -0
  20. package/src/anchor-generator.ts +80 -0
  21. package/src/api-server.ts +10 -0
  22. package/src/audit-log.ts +122 -0
  23. package/src/auto-gc.ts +98 -29
  24. package/src/auto-recall.ts +33 -4
  25. package/src/capture-engine.ts +372 -23
  26. package/src/cascade-forget.ts +3 -2
  27. package/src/cli.ts +107 -5
  28. package/src/compat.ts +22 -0
  29. package/src/confidence-tracker.ts +210 -47
  30. package/src/conflict-store.ts +12 -3
  31. package/src/consolidation-engine.ts +296 -8
  32. package/src/context-collapse-renderer.ts +222 -0
  33. package/src/context-composer-stable-selection.ts +5 -1
  34. package/src/context-composer.ts +239 -4
  35. package/src/context-reconstructor.ts +404 -0
  36. package/src/conversation-importer.ts +441 -0
  37. package/src/data-checkup.ts +232 -0
  38. package/src/decay-engine.ts +84 -0
  39. package/src/distill-lock.ts +122 -0
  40. package/src/doctor.ts +8 -10
  41. package/src/dream-pipeline.ts +227 -0
  42. package/src/emotion-detector.ts +90 -0
  43. package/src/eval.ts +3 -2
  44. package/src/forget-engine.ts +269 -0
  45. package/src/frequency-tracker.ts +175 -0
  46. package/src/graph-export.ts +644 -0
  47. package/src/ingest.ts +36 -6
  48. package/src/interference-detector.ts +187 -0
  49. package/src/kg-extractor.ts +15 -24
  50. package/src/kg-store.ts +2 -1
  51. package/src/language-hook.ts +67 -0
  52. package/src/llm-client.ts +289 -6
  53. package/src/mcp-server.ts +707 -80
  54. package/src/memory-assets.ts +22 -5
  55. package/src/memory-evolution.ts +365 -0
  56. package/src/memory-lint.ts +429 -0
  57. package/src/memory-output.ts +69 -0
  58. package/src/memory-schema.ts +74 -2
  59. package/src/narrative-schema.ts +79 -0
  60. package/src/narrative-tagger.ts +164 -0
  61. package/src/pii-detector.ts +98 -0
  62. package/src/post-retrieval-filter.ts +76 -0
  63. package/src/prediction-engine.ts +253 -0
  64. package/src/preference-slots.ts +65 -2
  65. package/src/prospective-memory.ts +215 -0
  66. package/src/query-expander.ts +67 -0
  67. package/src/recall-governor.ts +183 -0
  68. package/src/retention-policy.ts +90 -0
  69. package/src/retriever.ts +828 -55
  70. package/src/rif.ts +34 -8
  71. package/src/runtime-config.ts +23 -8
  72. package/src/scope-inventory.ts +4 -3
  73. package/src/scope-policy.ts +4 -0
  74. package/src/session-distiller.ts +307 -0
  75. package/src/session-output.ts +40 -0
  76. package/src/session-schema.ts +37 -0
  77. package/src/session-store.ts +7 -2
  78. package/src/skill-engine.ts +208 -0
  79. package/src/skill-promotion.ts +293 -0
  80. package/src/skill-schema.ts +39 -0
  81. package/src/store.ts +96 -10
  82. package/src/topic-tag.ts +66 -0
  83. package/src/tracker.ts +3 -1
  84. package/src/ui-server.ts +80 -8
  85. package/src/workflow-observation-scope-review.ts +2 -1
  86. package/src/workflow-observation-store.ts +2 -1
  87. package/src/write-verifier.ts +126 -0
  88. package/.claude-plugin/CLAUDE.md +0 -29
  89. package/.claude-plugin/marketplace.json +0 -18
  90. package/.github/workflows/ci.yml +0 -28
  91. package/.mcp.json +0 -9
  92. package/CHANGELOG.md +0 -51
  93. package/DEMO.md +0 -94
  94. package/EVAL.md +0 -89
  95. package/FAILURES.md +0 -75
  96. package/OPERATIONS.md +0 -59
  97. package/ROADMAP.md +0 -198
  98. package/assets/recallnest-workbench.png +0 -0
  99. package/assets/ui/app.js +0 -807
  100. package/assets/ui/index.html +0 -154
  101. package/assets/ui/styles.css +0 -605
  102. package/assets/wechat-qr.jpg +0 -0
  103. package/daily-reflections/2026-03-05.md +0 -15
  104. package/daily-reflections/2026-03-06.md +0 -15
  105. package/daily-reflections/2026-03-07.md +0 -7
  106. package/daily-reflections/2026-03-08.md +0 -34
  107. package/daily-reflections/2026-03-09.md +0 -35
  108. package/daily-reflections/2026-03-10.md +0 -36
  109. package/daily-reflections/2026-03-11.md +0 -35
  110. package/daily-reflections/2026-03-12.md +0 -36
  111. package/daily-reflections/2026-03-13.md +0 -35
  112. package/daily-reflections/2026-03-14.md +0 -35
  113. package/daily-reflections/2026-03-15.md +0 -35
  114. package/daily-reflections/2026-03-16.md +0 -41
  115. package/daily-reflections/2026-03-17.md +0 -10
  116. package/daily-reflections/2026-03-18.md +0 -10
  117. package/daily-reflections/2026-03-19.md +0 -10
  118. package/daily-reflections/2026-03-20.md +0 -10
  119. package/daily-reflections/2026-03-21.md +0 -10
  120. package/daily-reflections/2026-03-22.md +0 -10
  121. package/daily-reflections/2026-03-23.md +0 -10
  122. package/daily-reflections/2026-03-24.md +0 -10
  123. package/daily-reflections/2026-03-25.md +0 -32
  124. package/daily-reflections/2026-03-26.md +0 -37
  125. package/daily-reflections/2026-03-27.md +0 -35
  126. package/daily-reflections/2026-03-28.md +0 -35
  127. package/daily-reflections/2026-03-29.md +0 -35
  128. package/daily-reflections/2026-03-30.md +0 -36
  129. package/daily-reflections/2026-03-31.md +0 -35
  130. package/daily-reflections/2026-04-01.md +0 -35
  131. package/docs/api-reference.md +0 -1291
  132. package/docs/architecture.md +0 -168
  133. package/docs/memory-boundary-contract.md +0 -226
  134. package/docs/memory-categories.md +0 -119
  135. package/docs/pipeline-audit-ultramemory.md +0 -177
  136. package/docs/plans/2026-03-08-query-expansion.md +0 -270
  137. package/docs/plans/2026-03-16-continuity-layer.md +0 -196
  138. package/docs/plans/mcp-tiering-plan.md +0 -126
  139. package/docs/research/recall-quality-audit-2026-03-22.md +0 -153
  140. package/docs/self-evolution.md +0 -198
  141. package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +0 -782
  142. package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +0 -77
  143. package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +0 -95
  144. package/eval/continuity/results-2026-03-16.md +0 -117
  145. package/eval/continuity/results-2026-03-19.md +0 -453
  146. package/eval/continuity/runbook.md +0 -110
  147. package/eval/continuity/seed-cases.md +0 -54
  148. package/eval/reports/2026-03-06-abstract-query-baseline.md +0 -96
  149. package/eval/reports/2026-03-06-aws-ssh-baseline.md +0 -82
  150. package/eval/reports/2026-03-06-baseline.md +0 -82
  151. package/eval/reports/query-expansion.md +0 -138
  152. package/lm +0 -19
  153. package/plan-v1.2-distributable.md +0 -105
  154. package/scripts/archive-distilled-junk.ts +0 -116
  155. package/scripts/backfill-l0l1l2.ts +0 -98
  156. package/scripts/blind-scan.ts +0 -588
  157. package/scripts/cleanup-step1-exact-dups.ts +0 -117
  158. package/scripts/cleanup-step2-template-dups.ts +0 -113
  159. package/scripts/cleanup-step3-stale-content.ts +0 -149
  160. package/scripts/cleanup-step4-orphan-scopes.ts +0 -131
  161. package/scripts/delete-distilled-junk.ts +0 -88
  162. package/scripts/distill-facts.ts +0 -305
  163. package/scripts/health-check.ts +0 -343
  164. package/scripts/incremental-ingest.sh +0 -71
  165. package/scripts/mlp-health-check.ts +0 -382
  166. package/scripts/pull-claude-desktop.ts +0 -515
  167. package/scripts/re-ingest-cc.sh +0 -66
  168. package/scripts/rebalance-memory-health.ts +0 -181
  169. package/scripts/run-desktop-ingest.sh +0 -11
  170. package/scripts/start-server.sh +0 -12
  171. package/scripts/viz-dashboard.html +0 -423
  172. package/scripts/viz-export.ts +0 -703
  173. package/src/__tests__/access-tracker-novelty.test.ts +0 -228
  174. package/src/__tests__/auto-consolidation.test.ts +0 -168
  175. package/src/__tests__/auto-recall.test.ts +0 -131
  176. package/src/__tests__/batch-dedup.test.ts +0 -65
  177. package/src/__tests__/capture-engine.test.ts +0 -1142
  178. package/src/__tests__/capture-heuristic.test.ts +0 -171
  179. package/src/__tests__/cascade-forget.test.ts +0 -210
  180. package/src/__tests__/category-min-scores.test.ts +0 -33
  181. package/src/__tests__/confidence-tracker.test.ts +0 -196
  182. package/src/__tests__/conflict-advisor.test.ts +0 -206
  183. package/src/__tests__/conflict-escalation.test.ts +0 -149
  184. package/src/__tests__/conflict-lifecycle.test.ts +0 -83
  185. package/src/__tests__/conflict-output.test.ts +0 -85
  186. package/src/__tests__/conflict-store.test.ts +0 -374
  187. package/src/__tests__/consolidation-engine.test.ts +0 -181
  188. package/src/__tests__/context-composer-pins.test.ts +0 -64
  189. package/src/__tests__/context-composer-task-selection.test.ts +0 -24
  190. package/src/__tests__/context-renderer.test.ts +0 -91
  191. package/src/__tests__/continuity-eval.test.ts +0 -275
  192. package/src/__tests__/continuity-schema.test.ts +0 -97
  193. package/src/__tests__/core-recall-eval.test.ts +0 -235
  194. package/src/__tests__/core-summary.test.ts +0 -109
  195. package/src/__tests__/dedup-actions.test.ts +0 -58
  196. package/src/__tests__/deterministic-id.test.ts +0 -39
  197. package/src/__tests__/doctor.test.ts +0 -303
  198. package/src/__tests__/drill-down.test.ts +0 -108
  199. package/src/__tests__/embedder.test.ts +0 -122
  200. package/src/__tests__/entity-resolver.test.ts +0 -58
  201. package/src/__tests__/event-segmenter.test.ts +0 -153
  202. package/src/__tests__/hotness-blend.test.ts +0 -76
  203. package/src/__tests__/ingest.test.ts +0 -360
  204. package/src/__tests__/kg-extractor.test.ts +0 -184
  205. package/src/__tests__/kg-store.test.ts +0 -198
  206. package/src/__tests__/llm-consolidation.test.ts +0 -226
  207. package/src/__tests__/memory-boundaries.test.ts +0 -237
  208. package/src/__tests__/memory-health-rebalance.test.ts +0 -144
  209. package/src/__tests__/memory-output.test.ts +0 -215
  210. package/src/__tests__/multi-vector.test.ts +0 -242
  211. package/src/__tests__/ppr-traversal.test.ts +0 -256
  212. package/src/__tests__/preference-guard-smoke.test.ts +0 -211
  213. package/src/__tests__/preference-matcher.test.ts +0 -230
  214. package/src/__tests__/preference-slots.test.ts +0 -106
  215. package/src/__tests__/prospective-memory.test.ts +0 -283
  216. package/src/__tests__/query-entity-detector.test.ts +0 -171
  217. package/src/__tests__/query-expander.test.ts +0 -34
  218. package/src/__tests__/recall-mode.test.ts +0 -64
  219. package/src/__tests__/result-synthesizer.test.ts +0 -198
  220. package/src/__tests__/resume-context.test.ts +0 -4743
  221. package/src/__tests__/retrieval-trace.test.ts +0 -213
  222. package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
  223. package/src/__tests__/retroactive-boost.test.ts +0 -223
  224. package/src/__tests__/scope-inventory.test.ts +0 -200
  225. package/src/__tests__/scope-policy.test.ts +0 -69
  226. package/src/__tests__/session-checkpoint.test.ts +0 -259
  227. package/src/__tests__/session-gc.test.ts +0 -215
  228. package/src/__tests__/store-scope-audit.test.ts +0 -81
  229. package/src/__tests__/temporal-parser.test.ts +0 -241
  230. package/src/__tests__/term-registry.test.ts +0 -121
  231. package/src/__tests__/tool-output-compressor.test.ts +0 -133
  232. package/src/__tests__/version-manager.test.ts +0 -213
  233. 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
+ }