recallnest 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/.claude-plugin/CLAUDE.md +29 -0
  2. package/.claude-plugin/marketplace.json +18 -0
  3. package/.env.example +3 -0
  4. package/.github/workflows/ci.yml +28 -0
  5. package/.mcp.json +9 -0
  6. package/CHANGELOG.md +51 -0
  7. package/CLAUDE.md +42 -0
  8. package/DEMO.md +94 -0
  9. package/EVAL.md +89 -0
  10. package/FAILURES.md +75 -0
  11. package/LICENSE +21 -0
  12. package/OPERATIONS.md +59 -0
  13. package/README.md +427 -0
  14. package/README_CN.md +425 -0
  15. package/ROADMAP.md +198 -0
  16. package/assets/recallnest-workbench.png +0 -0
  17. package/assets/ui/app.js +807 -0
  18. package/assets/ui/index.html +154 -0
  19. package/assets/ui/styles.css +605 -0
  20. package/assets/wechat-qr.jpg +0 -0
  21. package/config.json.example +38 -0
  22. package/daily-recall.sh +87 -0
  23. package/daily-reflections/2026-03-05.md +15 -0
  24. package/daily-reflections/2026-03-06.md +15 -0
  25. package/daily-reflections/2026-03-07.md +7 -0
  26. package/daily-reflections/2026-03-08.md +34 -0
  27. package/daily-reflections/2026-03-09.md +35 -0
  28. package/daily-reflections/2026-03-10.md +36 -0
  29. package/daily-reflections/2026-03-11.md +35 -0
  30. package/daily-reflections/2026-03-12.md +36 -0
  31. package/daily-reflections/2026-03-13.md +35 -0
  32. package/daily-reflections/2026-03-14.md +35 -0
  33. package/daily-reflections/2026-03-15.md +35 -0
  34. package/daily-reflections/2026-03-16.md +41 -0
  35. package/daily-reflections/2026-03-17.md +10 -0
  36. package/daily-reflections/2026-03-18.md +10 -0
  37. package/daily-reflections/2026-03-19.md +10 -0
  38. package/daily-reflections/2026-03-20.md +10 -0
  39. package/daily-reflections/2026-03-21.md +10 -0
  40. package/daily-reflections/2026-03-22.md +10 -0
  41. package/daily-reflections/2026-03-23.md +10 -0
  42. package/daily-reflections/2026-03-24.md +10 -0
  43. package/daily-reflections/2026-03-25.md +32 -0
  44. package/daily-reflections/2026-03-26.md +37 -0
  45. package/daily-reflections/2026-03-27.md +35 -0
  46. package/daily-reflections/2026-03-28.md +35 -0
  47. package/daily-reflections/2026-03-29.md +35 -0
  48. package/daily-reflections/2026-03-30.md +36 -0
  49. package/daily-reflections/2026-03-31.md +35 -0
  50. package/daily-reflections/2026-04-01.md +35 -0
  51. package/docs/api-reference.md +1291 -0
  52. package/docs/architecture.md +168 -0
  53. package/docs/memory-boundary-contract.md +226 -0
  54. package/docs/memory-categories.md +119 -0
  55. package/docs/pipeline-audit-ultramemory.md +177 -0
  56. package/docs/plans/2026-03-08-query-expansion.md +270 -0
  57. package/docs/plans/2026-03-16-continuity-layer.md +196 -0
  58. package/docs/plans/mcp-tiering-plan.md +126 -0
  59. package/docs/research/recall-quality-audit-2026-03-22.md +153 -0
  60. package/docs/self-evolution.md +198 -0
  61. package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +782 -0
  62. package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +77 -0
  63. package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +95 -0
  64. package/eval/cases.json +85 -0
  65. package/eval/continuity/case-seeds.json +90 -0
  66. package/eval/continuity/cases.json +302 -0
  67. package/eval/continuity/memory-seeds.json +26 -0
  68. package/eval/continuity/pattern-seeds.json +100 -0
  69. package/eval/continuity/results-2026-03-16.md +117 -0
  70. package/eval/continuity/results-2026-03-19.md +453 -0
  71. package/eval/continuity/runbook.md +110 -0
  72. package/eval/continuity/seed-cases.md +54 -0
  73. package/eval/reports/2026-03-06-abstract-query-baseline.md +96 -0
  74. package/eval/reports/2026-03-06-aws-ssh-baseline.md +82 -0
  75. package/eval/reports/2026-03-06-baseline.md +82 -0
  76. package/eval/reports/query-expansion.md +138 -0
  77. package/integrations/README.md +57 -0
  78. package/integrations/claude-code/README.md +133 -0
  79. package/integrations/claude-code/claude-md-snippet.md +31 -0
  80. package/integrations/claude-code/setup.sh +66 -0
  81. package/integrations/claude-code/smoke-continuity.sh +452 -0
  82. package/integrations/codex/README.md +49 -0
  83. package/integrations/codex/agents-md-snippet.md +31 -0
  84. package/integrations/codex/setup.sh +58 -0
  85. package/integrations/examples/claude-agent-sdk/memory-agent.ts +182 -0
  86. package/integrations/examples/langchain/memory-chain.py +111 -0
  87. package/integrations/examples/openai-agents-sdk/memory-agent.py +109 -0
  88. package/integrations/gemini-cli/README.md +57 -0
  89. package/integrations/gemini-cli/gemini-md-snippet.md +31 -0
  90. package/integrations/gemini-cli/setup.sh +64 -0
  91. package/integrations/shared/install-managed-block.sh +51 -0
  92. package/lm +19 -0
  93. package/package.json +50 -0
  94. package/plan-v1.2-distributable.md +105 -0
  95. package/scripts/archive-distilled-junk.ts +116 -0
  96. package/scripts/backfill-l0l1l2.ts +98 -0
  97. package/scripts/blind-scan.ts +588 -0
  98. package/scripts/cleanup-step1-exact-dups.ts +117 -0
  99. package/scripts/cleanup-step2-template-dups.ts +113 -0
  100. package/scripts/cleanup-step3-stale-content.ts +149 -0
  101. package/scripts/cleanup-step4-orphan-scopes.ts +131 -0
  102. package/scripts/delete-distilled-junk.ts +88 -0
  103. package/scripts/distill-facts.ts +305 -0
  104. package/scripts/health-check.ts +343 -0
  105. package/scripts/incremental-ingest.sh +71 -0
  106. package/scripts/mlp-health-check.ts +382 -0
  107. package/scripts/pull-claude-desktop.ts +515 -0
  108. package/scripts/re-ingest-cc.sh +66 -0
  109. package/scripts/rebalance-memory-health.ts +181 -0
  110. package/scripts/run-desktop-ingest.sh +11 -0
  111. package/scripts/start-server.sh +12 -0
  112. package/scripts/viz-dashboard.html +423 -0
  113. package/scripts/viz-export.ts +703 -0
  114. package/src/__tests__/access-tracker-novelty.test.ts +228 -0
  115. package/src/__tests__/auto-consolidation.test.ts +168 -0
  116. package/src/__tests__/auto-recall.test.ts +131 -0
  117. package/src/__tests__/batch-dedup.test.ts +65 -0
  118. package/src/__tests__/capture-engine.test.ts +1142 -0
  119. package/src/__tests__/capture-heuristic.test.ts +171 -0
  120. package/src/__tests__/cascade-forget.test.ts +210 -0
  121. package/src/__tests__/category-min-scores.test.ts +33 -0
  122. package/src/__tests__/confidence-tracker.test.ts +196 -0
  123. package/src/__tests__/conflict-advisor.test.ts +206 -0
  124. package/src/__tests__/conflict-escalation.test.ts +149 -0
  125. package/src/__tests__/conflict-lifecycle.test.ts +83 -0
  126. package/src/__tests__/conflict-output.test.ts +85 -0
  127. package/src/__tests__/conflict-store.test.ts +374 -0
  128. package/src/__tests__/consolidation-engine.test.ts +181 -0
  129. package/src/__tests__/context-composer-pins.test.ts +64 -0
  130. package/src/__tests__/context-composer-task-selection.test.ts +24 -0
  131. package/src/__tests__/context-renderer.test.ts +91 -0
  132. package/src/__tests__/continuity-eval.test.ts +275 -0
  133. package/src/__tests__/continuity-schema.test.ts +97 -0
  134. package/src/__tests__/core-recall-eval.test.ts +235 -0
  135. package/src/__tests__/core-summary.test.ts +109 -0
  136. package/src/__tests__/dedup-actions.test.ts +58 -0
  137. package/src/__tests__/deterministic-id.test.ts +39 -0
  138. package/src/__tests__/doctor.test.ts +303 -0
  139. package/src/__tests__/drill-down.test.ts +108 -0
  140. package/src/__tests__/embedder.test.ts +122 -0
  141. package/src/__tests__/entity-resolver.test.ts +58 -0
  142. package/src/__tests__/event-segmenter.test.ts +153 -0
  143. package/src/__tests__/hotness-blend.test.ts +76 -0
  144. package/src/__tests__/ingest.test.ts +360 -0
  145. package/src/__tests__/kg-extractor.test.ts +184 -0
  146. package/src/__tests__/kg-store.test.ts +198 -0
  147. package/src/__tests__/llm-consolidation.test.ts +226 -0
  148. package/src/__tests__/memory-boundaries.test.ts +237 -0
  149. package/src/__tests__/memory-health-rebalance.test.ts +144 -0
  150. package/src/__tests__/memory-output.test.ts +215 -0
  151. package/src/__tests__/multi-vector.test.ts +242 -0
  152. package/src/__tests__/ppr-traversal.test.ts +256 -0
  153. package/src/__tests__/preference-guard-smoke.test.ts +211 -0
  154. package/src/__tests__/preference-matcher.test.ts +230 -0
  155. package/src/__tests__/preference-slots.test.ts +106 -0
  156. package/src/__tests__/prospective-memory.test.ts +283 -0
  157. package/src/__tests__/query-entity-detector.test.ts +171 -0
  158. package/src/__tests__/query-expander.test.ts +34 -0
  159. package/src/__tests__/recall-mode.test.ts +64 -0
  160. package/src/__tests__/result-synthesizer.test.ts +198 -0
  161. package/src/__tests__/resume-context.test.ts +4743 -0
  162. package/src/__tests__/retrieval-trace.test.ts +213 -0
  163. package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
  164. package/src/__tests__/retroactive-boost.test.ts +223 -0
  165. package/src/__tests__/scope-inventory.test.ts +200 -0
  166. package/src/__tests__/scope-policy.test.ts +69 -0
  167. package/src/__tests__/session-checkpoint.test.ts +259 -0
  168. package/src/__tests__/session-gc.test.ts +215 -0
  169. package/src/__tests__/store-scope-audit.test.ts +81 -0
  170. package/src/__tests__/temporal-parser.test.ts +241 -0
  171. package/src/__tests__/term-registry.test.ts +121 -0
  172. package/src/__tests__/tool-output-compressor.test.ts +133 -0
  173. package/src/__tests__/version-manager.test.ts +213 -0
  174. package/src/__tests__/workflow-observation.test.ts +400 -0
  175. package/src/access-tracker.ts +316 -0
  176. package/src/adaptive-retrieval.ts +64 -0
  177. package/src/api-server.ts +667 -0
  178. package/src/asset-sync.ts +79 -0
  179. package/src/auto-consolidation.ts +133 -0
  180. package/src/auto-gc.ts +124 -0
  181. package/src/auto-recall.ts +95 -0
  182. package/src/capture-engine.ts +931 -0
  183. package/src/capture-heuristic.ts +177 -0
  184. package/src/cascade-forget.ts +135 -0
  185. package/src/chunker.ts +253 -0
  186. package/src/cli.ts +1917 -0
  187. package/src/confidence-tracker.ts +166 -0
  188. package/src/conflict-advisor.ts +433 -0
  189. package/src/conflict-engine.ts +181 -0
  190. package/src/conflict-escalation.ts +138 -0
  191. package/src/conflict-lifecycle.ts +67 -0
  192. package/src/conflict-output.ts +218 -0
  193. package/src/conflict-schema.ts +130 -0
  194. package/src/conflict-store.ts +127 -0
  195. package/src/consolidation-engine.ts +256 -0
  196. package/src/context-composer-pins.ts +183 -0
  197. package/src/context-composer-queries.ts +121 -0
  198. package/src/context-composer-scope.ts +56 -0
  199. package/src/context-composer-stable-selection.ts +200 -0
  200. package/src/context-composer-stable.ts +217 -0
  201. package/src/context-composer-task-fallbacks.ts +36 -0
  202. package/src/context-composer-task-ranking.ts +197 -0
  203. package/src/context-composer-task-results.ts +137 -0
  204. package/src/context-composer-task-selection.ts +342 -0
  205. package/src/context-composer-text.ts +57 -0
  206. package/src/context-composer.ts +300 -0
  207. package/src/context-renderer.ts +179 -0
  208. package/src/decay-engine.ts +189 -0
  209. package/src/doctor.ts +543 -0
  210. package/src/embedder.ts +557 -0
  211. package/src/entity-resolver.ts +131 -0
  212. package/src/eval.ts +632 -0
  213. package/src/event-segmenter.ts +216 -0
  214. package/src/ingest.ts +1822 -0
  215. package/src/kg-extractor.ts +217 -0
  216. package/src/kg-store.ts +353 -0
  217. package/src/legacy-scope-review.ts +53 -0
  218. package/src/llm-client.ts +629 -0
  219. package/src/llm-consolidation.ts +168 -0
  220. package/src/mcp-server.ts +1337 -0
  221. package/src/memory-assets.ts +475 -0
  222. package/src/memory-boundaries.ts +354 -0
  223. package/src/memory-health-rebalance.ts +220 -0
  224. package/src/memory-output.ts +474 -0
  225. package/src/memory-schema.ts +184 -0
  226. package/src/multi-vector.ts +234 -0
  227. package/src/noise-filter.ts +155 -0
  228. package/src/ppr-traversal.ts +279 -0
  229. package/src/preference-matcher.ts +174 -0
  230. package/src/preference-slots.ts +259 -0
  231. package/src/prospective-memory.ts +198 -0
  232. package/src/query-entity-detector.ts +207 -0
  233. package/src/query-expander.ts +98 -0
  234. package/src/result-synthesizer.ts +114 -0
  235. package/src/retrieval-profiles.ts +101 -0
  236. package/src/retrieval-trace.ts +94 -0
  237. package/src/retriever.ts +1338 -0
  238. package/src/retroactive-boost.ts +154 -0
  239. package/src/rif.ts +73 -0
  240. package/src/runtime-config.ts +172 -0
  241. package/src/schema-utils.ts +56 -0
  242. package/src/scope-inventory.ts +310 -0
  243. package/src/scope-policy.ts +142 -0
  244. package/src/session-engine.ts +168 -0
  245. package/src/session-output.ts +96 -0
  246. package/src/session-schema.ts +60 -0
  247. package/src/session-store.ts +293 -0
  248. package/src/stderr-log.ts +16 -0
  249. package/src/store.ts +903 -0
  250. package/src/temporal-parser.ts +318 -0
  251. package/src/term-registry.ts +791 -0
  252. package/src/tool-output-compressor.ts +115 -0
  253. package/src/tracker.ts +57 -0
  254. package/src/ui-server.ts +430 -0
  255. package/src/version-manager.ts +172 -0
  256. package/src/workflow-observation-engine.ts +316 -0
  257. package/src/workflow-observation-managed.ts +83 -0
  258. package/src/workflow-observation-output.ts +86 -0
  259. package/src/workflow-observation-schema.ts +87 -0
  260. package/src/workflow-observation-scope-review.ts +409 -0
  261. package/src/workflow-observation-store.ts +69 -0
@@ -0,0 +1,57 @@
1
+ import { normalizeText } from "./term-registry.js";
2
+
3
+ export function compactWhitespace(text: string): string {
4
+ return text.replace(/\s+/g, " ").trim();
5
+ }
6
+
7
+ export function cleanText(text: string, maxLen: number): string {
8
+ const compact = compactWhitespace(text);
9
+ if (compact.length <= maxLen) return compact;
10
+ return `${compact.slice(0, Math.max(0, maxLen - 3)).trimEnd()}...`;
11
+ }
12
+
13
+ export function stripConversationMarkers(text: string): string {
14
+ return text
15
+ .replace(/<image[^>]*>\s*/gi, "")
16
+ .replace(/\[(用户|助手|Pinned Asset|Memory Brief)\]\s*/g, "")
17
+ .replace(/\bSummary:\s*/gi, "")
18
+ .replace(/\bSnippet:\s*/gi, "")
19
+ .replace(/\s+/g, " ")
20
+ .trim();
21
+ }
22
+
23
+ /**
24
+ * Tier 3.1: Extract the best available summary text from a memory entry.
25
+ * Priority: core_summary > l1_overview > l0_abstract > raw text.
26
+ * Falls back to raw text (stripped of conversation markers) when no summaries exist.
27
+ */
28
+ export function bestSummaryText(text: string, metadata?: string): string {
29
+ if (metadata) {
30
+ try {
31
+ const meta = JSON.parse(metadata);
32
+ if (typeof meta.core_summary === "string" && meta.core_summary.length > 0) {
33
+ return meta.core_summary;
34
+ }
35
+ if (typeof meta.l1_overview === "string" && meta.l1_overview.length > 0) {
36
+ return meta.l1_overview;
37
+ }
38
+ if (typeof meta.l0_abstract === "string" && meta.l0_abstract.length > 0) {
39
+ return meta.l0_abstract;
40
+ }
41
+ } catch { /* metadata parse failed, use raw text */ }
42
+ }
43
+ return stripConversationMarkers(text);
44
+ }
45
+
46
+ export function dedupeText(items: string[], limit: number): string[] {
47
+ const seen = new Set<string>();
48
+ const output: string[] = [];
49
+ for (const item of items) {
50
+ const key = normalizeText(item);
51
+ if (!key || seen.has(key)) continue;
52
+ seen.add(key);
53
+ output.push(item);
54
+ if (output.length >= limit) break;
55
+ }
56
+ return output;
57
+ }
@@ -0,0 +1,300 @@
1
+ import { listPinAssets, type PinAsset } from "./memory-assets.js";
2
+ import { buildStableContextSections } from "./context-composer-stable.js";
3
+ import {
4
+ buildAssociativeNestEntityFallbackQuery,
5
+ buildScopedEntityFallbackQuery,
6
+ buildStableQuery,
7
+ buildStylePreferenceFallbackQuery,
8
+ buildTaskQuery,
9
+ formatLatestCheckpointHeadline,
10
+ } from "./context-composer-queries.js";
11
+ import { cleanText, dedupeText } from "./context-composer-text.js";
12
+ import {
13
+ buildTaskResultSections,
14
+ } from "./context-composer-task-results.js";
15
+ import type { RetrievalContext, RetrievalResult } from "./retriever.js";
16
+ import type { ResumeContextResponse, SessionCheckpointRecord } from "./session-schema.js";
17
+ import { ResumeContextRequestSchema, ResumeContextResponseSchema } from "./session-schema.js";
18
+ import { formatCheckpointRecallSummary } from "./session-output.js";
19
+ import {
20
+ STRONG_WORKFLOW_CUE_TERMS,
21
+ looksLikeContinuityTask,
22
+ looksLikeRecallOnlyTask,
23
+ looksLikeStyleTask,
24
+ } from "./term-registry.js";
25
+ import { synthesizeSection } from "./result-synthesizer.js";
26
+ import type { LLMClient } from "./llm-client.js";
27
+ type ResumeCategory = "profile" | "preferences" | "entities" | "patterns" | "cases";
28
+
29
+ interface ResumeRetriever {
30
+ retrieve(context: RetrievalContext): Promise<RetrievalResult[]>;
31
+ }
32
+
33
+ interface CheckpointLookup {
34
+ getLatest(query?: { sessionId?: string; scope?: string }): Promise<SessionCheckpointRecord | null>;
35
+ }
36
+
37
+ export interface ResumeContextDeps {
38
+ retriever: ResumeRetriever;
39
+ checkpointStore: CheckpointLookup;
40
+ listPins?: (limit?: number) => Array<PinAsset & { path: string }>;
41
+ /** Optional LLM client for Tier 3.5 result synthesis. */
42
+ llm?: LLMClient | null;
43
+ }
44
+
45
+ async function retrieveCandidates(
46
+ retriever: ResumeRetriever,
47
+ params: {
48
+ category?: ResumeCategory;
49
+ query: string;
50
+ limit: number;
51
+ scope?: string;
52
+ },
53
+ ): Promise<RetrievalResult[]> {
54
+ const { category, query, limit, scope } = params;
55
+
56
+ if (!scope) {
57
+ return retriever.retrieve({
58
+ query,
59
+ limit,
60
+ ...(category ? { category } : {}),
61
+ source: "auto-recall",
62
+ });
63
+ }
64
+
65
+ const [scoped, global] = await Promise.all([
66
+ retriever.retrieve({
67
+ query,
68
+ limit,
69
+ ...(category ? { category } : {}),
70
+ scopeFilter: [scope],
71
+ source: "auto-recall",
72
+ }),
73
+ retriever.retrieve({
74
+ query,
75
+ limit: Math.min(10, limit * 2),
76
+ ...(category ? { category } : {}),
77
+ source: "auto-recall",
78
+ }),
79
+ ]);
80
+
81
+ const seen = new Set<string>();
82
+ const merged: RetrievalResult[] = [];
83
+ for (const result of [...scoped, ...global]) {
84
+ if (seen.has(result.entry.id)) continue;
85
+ seen.add(result.entry.id);
86
+ merged.push(result);
87
+ if (merged.length >= Math.min(10, limit * 2)) break;
88
+ }
89
+ return merged;
90
+ }
91
+
92
+ function mergeRetrievalResults(resultSets: RetrievalResult[][], limit: number): RetrievalResult[] {
93
+ const seen = new Set<string>();
94
+ const merged: RetrievalResult[] = [];
95
+ for (const set of resultSets) {
96
+ for (const result of set) {
97
+ if (seen.has(result.entry.id)) continue;
98
+ seen.add(result.entry.id);
99
+ merged.push(result);
100
+ if (merged.length >= limit) return merged;
101
+ }
102
+ }
103
+ return merged;
104
+ }
105
+
106
+ async function resolveLatestCheckpoint(
107
+ checkpointStore: CheckpointLookup,
108
+ params: {
109
+ includeLatestCheckpoint: boolean;
110
+ sessionId?: string;
111
+ scope?: string;
112
+ },
113
+ ): Promise<SessionCheckpointRecord | null> {
114
+ if (!params.includeLatestCheckpoint) return null;
115
+
116
+ if (params.sessionId) {
117
+ const bySession = await checkpointStore.getLatest({ sessionId: params.sessionId });
118
+ if (bySession) return bySession;
119
+ }
120
+
121
+ if (params.scope) {
122
+ return checkpointStore.getLatest({ scope: params.scope });
123
+ }
124
+
125
+ return null;
126
+ }
127
+
128
+ function buildSummary(params: {
129
+ stableContext: string[];
130
+ relevantPatterns: string[];
131
+ recentCases: string[];
132
+ latestCheckpoint: SessionCheckpointRecord | null;
133
+ }): string {
134
+ const parts: string[] = [];
135
+
136
+ if (params.latestCheckpoint) {
137
+ parts.push(
138
+ formatLatestCheckpointHeadline(
139
+ params.latestCheckpoint.sessionId,
140
+ params.latestCheckpoint.updatedAt,
141
+ formatCheckpointRecallSummary(params.latestCheckpoint),
142
+ ),
143
+ );
144
+ }
145
+
146
+ if (params.stableContext.length > 0) {
147
+ parts.push(`Stable context: ${params.stableContext.slice(0, 2).map((item) => cleanText(item, 120)).join(" | ")}`);
148
+ }
149
+
150
+ parts.push(
151
+ `Loaded ${params.stableContext.length} stable context item(s), ${params.relevantPatterns.length} pattern(s), and ${params.recentCases.length} case(s).`,
152
+ );
153
+
154
+ return cleanText(parts.join(" "), 800);
155
+ }
156
+
157
+ export async function composeResumeContext(
158
+ deps: ResumeContextDeps,
159
+ rawInput: unknown,
160
+ ): Promise<ResumeContextResponse> {
161
+ const input = ResumeContextRequestSchema.parse(rawInput);
162
+ const latestCheckpoint = await resolveLatestCheckpoint(deps.checkpointStore, {
163
+ includeLatestCheckpoint: input.includeLatestCheckpoint,
164
+ sessionId: input.sessionId,
165
+ scope: input.scope,
166
+ });
167
+
168
+ const resolvedScope = input.scope || latestCheckpoint?.resolvedScope;
169
+ const taskSeed = input.task || latestCheckpoint?.task || latestCheckpoint?.summary;
170
+ const stableLimit = input.limitPerSection;
171
+ const taskLimit = input.limitPerSection;
172
+ const styleFocusedTask = looksLikeStyleTask(taskSeed) || input.profile === "writing";
173
+ const recallOnlyTask = looksLikeRecallOnlyTask(taskSeed) && styleFocusedTask;
174
+
175
+ const preferenceQueries = dedupeText([
176
+ buildStableQuery("preferences", taskSeed),
177
+ ...(styleFocusedTask ? [buildStylePreferenceFallbackQuery(taskSeed)] : []),
178
+ ], 2);
179
+ const entityQueries = dedupeText([
180
+ buildStableQuery("entities", taskSeed),
181
+ ...(resolvedScope ? [buildScopedEntityFallbackQuery(resolvedScope, taskSeed)] : []),
182
+ ...(!resolvedScope ? [buildAssociativeNestEntityFallbackQuery(taskSeed)] : []),
183
+ ], 3);
184
+
185
+ const [profileResults, preferenceResultSets, entityResultSets, patternResults, caseResults] = await Promise.all([
186
+ retrieveCandidates(deps.retriever, {
187
+ category: "profile",
188
+ query: buildStableQuery("profile", taskSeed),
189
+ limit: Math.max(2, stableLimit),
190
+ scope: resolvedScope,
191
+ }),
192
+ Promise.all(preferenceQueries.map((query) =>
193
+ retrieveCandidates(deps.retriever, {
194
+ category: "preferences",
195
+ query,
196
+ limit: Math.max(2, stableLimit),
197
+ scope: resolvedScope,
198
+ })
199
+ )),
200
+ Promise.all(entityQueries.map((query) =>
201
+ retrieveCandidates(deps.retriever, {
202
+ category: "entities",
203
+ query,
204
+ limit: Math.max(2, stableLimit),
205
+ scope: resolvedScope,
206
+ })
207
+ )),
208
+ retrieveCandidates(deps.retriever, {
209
+ category: "patterns",
210
+ query: buildTaskQuery("patterns", taskSeed),
211
+ limit: taskLimit,
212
+ scope: resolvedScope,
213
+ }),
214
+ retrieveCandidates(deps.retriever, {
215
+ category: "cases",
216
+ query: buildTaskQuery("cases", taskSeed),
217
+ limit: taskLimit,
218
+ scope: resolvedScope,
219
+ }),
220
+ ]);
221
+ const preferenceResults = mergeRetrievalResults(preferenceResultSets, Math.max(4, stableLimit * 3));
222
+ const entityResults = mergeRetrievalResults(entityResultSets, Math.max(4, stableLimit * 3));
223
+
224
+ const continuityTask = looksLikeContinuityTask(taskSeed);
225
+ const pinAssets = (deps.listPins || listPinAssets)(Math.max(4, stableLimit * 2));
226
+ const {
227
+ preferenceContext,
228
+ stableContext,
229
+ } = buildStableContextSections({
230
+ profileResults,
231
+ preferenceResults,
232
+ entityResults,
233
+ pinAssets,
234
+ latestCheckpoint,
235
+ taskSeed,
236
+ scope: resolvedScope,
237
+ stableLimit,
238
+ styleFocusedTask,
239
+ });
240
+
241
+ const { relevantPatterns, recentCases } = await buildTaskResultSections({
242
+ retrieveCandidates: ({ category, query, limit, scope }) =>
243
+ retrieveCandidates(deps.retriever, {
244
+ ...(category ? { category } : {}),
245
+ query,
246
+ limit,
247
+ scope,
248
+ }),
249
+ patternResults,
250
+ caseResults,
251
+ continuityTask,
252
+ hasLatestCheckpoint: Boolean(latestCheckpoint),
253
+ taskLimit,
254
+ taskSeed,
255
+ scope: resolvedScope,
256
+ strongWorkflowCueTerms: STRONG_WORKFLOW_CUE_TERMS,
257
+ });
258
+
259
+ // Tier 3.5: Optionally synthesize sections into coherent narratives via LLM.
260
+ // Only activates when RECALLNEST_SYNTHESIZE=true and llm is provided.
261
+ const llm = deps.llm ?? null;
262
+ const queryHint = taskSeed || resolvedScope || "general";
263
+ const [synthStable, synthPatterns, synthCases] = await Promise.all([
264
+ synthesizeSection(stableContext, queryHint, llm),
265
+ synthesizeSection(relevantPatterns, queryHint, llm),
266
+ synthesizeSection(recentCases, queryHint, llm),
267
+ ]);
268
+
269
+ const response = {
270
+ summary: buildSummary({
271
+ stableContext: synthStable,
272
+ relevantPatterns: synthPatterns,
273
+ recentCases: synthCases,
274
+ latestCheckpoint,
275
+ }),
276
+ resolvedScope,
277
+ stableContext: synthStable,
278
+ relevantPatterns: synthPatterns,
279
+ recentCases: synthCases,
280
+ latestCheckpoint: latestCheckpoint
281
+ ? {
282
+ sessionId: latestCheckpoint.sessionId,
283
+ resolvedScope: latestCheckpoint.resolvedScope,
284
+ summary: formatCheckpointRecallSummary(latestCheckpoint),
285
+ updatedAt: latestCheckpoint.updatedAt,
286
+ }
287
+ : undefined,
288
+ responseMode: recallOnlyTask ? "recall-only" as const : "default" as const,
289
+ responseGuidance: recallOnlyTask
290
+ ? (
291
+ stableContext.length <= 1
292
+ ? "Recall-only mode: answer from the recalled stable context item only. Restate it briefly and do not expand into extra rules, examples, or local writing docs unless the user explicitly asks."
293
+ : "Recall-only mode: answer only from the recalled stable context items. Keep the reply brief and do not expand into extra rules, examples, or local writing docs unless the user explicitly asks."
294
+ )
295
+ : undefined,
296
+ generatedAt: new Date().toISOString(),
297
+ };
298
+
299
+ return ResumeContextResponseSchema.parse(response);
300
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Context Renderer — reorder recall results by contextual relevance.
3
+ *
4
+ * Borrowed from UltraMemory's context-renderer.ts, adapted for RecallNest:
5
+ * - "verbatim" mode: pass-through (default, backward-compatible)
6
+ * - "highlight" mode: reorder by 60% vector score + 40% term overlap
7
+ * - "synthesize" mode: reserved for future LLM-based rendering (falls back to highlight)
8
+ *
9
+ * Pure functions, zero dependencies on store/embedder/LLM.
10
+ */
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Types
14
+ // ---------------------------------------------------------------------------
15
+
16
+ export type RenderMode = "verbatim" | "highlight" | "synthesize";
17
+
18
+ export interface RenderableMemory {
19
+ id: string;
20
+ text: string;
21
+ score: number;
22
+ category: string;
23
+ }
24
+
25
+ export interface RenderedMemory {
26
+ id: string;
27
+ text: string;
28
+ category: string;
29
+ /** Contextual relevance score (0–1), combining vector score + term overlap. */
30
+ relevance: number;
31
+ }
32
+
33
+ export interface RenderResult {
34
+ mode: RenderMode;
35
+ memories: RenderedMemory[];
36
+ }
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Stop words (EN + ZH function words)
40
+ // ---------------------------------------------------------------------------
41
+
42
+ const STOP_WORDS = new Set([
43
+ // English
44
+ "the", "a", "an", "is", "are", "was", "were", "be", "been", "being",
45
+ "have", "has", "had", "do", "does", "did", "will", "would", "could",
46
+ "should", "may", "might", "shall", "can", "need", "to", "of", "in",
47
+ "for", "on", "with", "at", "by", "from", "as", "into", "through",
48
+ "during", "before", "after", "about", "between", "under", "above",
49
+ "this", "that", "these", "those", "it", "its", "i", "me", "my",
50
+ "we", "our", "you", "your", "he", "she", "they", "them", "and",
51
+ "or", "but", "if", "then", "so", "just", "also", "not", "no",
52
+ // Chinese function words
53
+ "的", "了", "在", "是", "我", "有", "和", "就", "不", "人",
54
+ "都", "一", "一个", "上", "也", "很", "到", "说", "要", "去",
55
+ "你", "会", "着", "没有", "看", "好", "自己", "这",
56
+ ]);
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Term extraction
60
+ // ---------------------------------------------------------------------------
61
+
62
+ /** Extract significant terms from text (lowercase, deduped, stop words removed). */
63
+ export function extractTerms(text: string): Set<string> {
64
+ const words = text
65
+ .toLowerCase()
66
+ .replace(/[^\w\s\u4e00-\u9fff]/g, " ")
67
+ .split(/\s+/);
68
+ return new Set(words.filter(w => w.length > 1 && !STOP_WORDS.has(w)));
69
+ }
70
+
71
+ /** Compute Jaccard-like overlap between two term sets. */
72
+ export function computeTermOverlap(a: Set<string>, b: Set<string>): number {
73
+ if (a.size === 0 || b.size === 0) return 0;
74
+ let overlap = 0;
75
+ for (const term of a) {
76
+ if (b.has(term)) overlap++;
77
+ }
78
+ return overlap / Math.max(a.size, b.size);
79
+ }
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // Temporal helpers
83
+ // ---------------------------------------------------------------------------
84
+
85
+ /** Detect whether a query asks for a specific date/time. */
86
+ export function isTemporalQuery(query: string): boolean {
87
+ return /^when\b|what (date|time|day|year|month)\b|how long ago\b/i.test(query);
88
+ }
89
+
90
+ /** Extract a bracketed date anchor from memory text, e.g. "[1:56 pm on 7 May, 2023]". */
91
+ function extractAnchorDate(text: string): { dateStr: string; year: number; month: number } | null {
92
+ const m = text.match(/\[(?:\d{1,2}:\d{2}\s*(?:am|pm)\s+on\s+)?(\d{1,2})\s+(\w+),?\s+(\d{4})\]/i);
93
+ if (!m) return null;
94
+ const monthMap: Record<string, number> = {
95
+ january: 1, february: 2, march: 3, april: 4, may: 5, june: 6,
96
+ july: 7, august: 8, september: 9, october: 10, november: 11, december: 12,
97
+ };
98
+ const month = monthMap[m[2].toLowerCase()];
99
+ if (!month) return null;
100
+ return { dateStr: m[0], year: parseInt(m[3], 10), month };
101
+ }
102
+
103
+ /**
104
+ * Annotate relative time expressions with absolute dates.
105
+ * E.g. "last year" in text dated [7 May, 2023] → "last year (i.e., 2022)"
106
+ */
107
+ export function resolveRelativeDates(text: string): string {
108
+ const anchor = extractAnchorDate(text);
109
+ if (!anchor) return text;
110
+
111
+ const { year, month } = anchor;
112
+ const MONTHS = ["", "January", "February", "March", "April", "May", "June",
113
+ "July", "August", "September", "October", "November", "December"];
114
+
115
+ return text
116
+ .replace(/\blast year\b/gi, `last year (i.e., ${year - 1})`)
117
+ .replace(/\bnext year\b/gi, `next year (i.e., ${year + 1})`)
118
+ .replace(/\blast month\b/gi, `last month (i.e., ${MONTHS[month === 1 ? 12 : month - 1]} ${month === 1 ? year - 1 : year})`)
119
+ .replace(/\bnext month\b/gi, `next month (i.e., ${MONTHS[month === 12 ? 1 : month + 1]} ${month === 12 ? year + 1 : year})`)
120
+ .replace(/\b(\d+)\s+years?\s+ago\b/gi, (match, n) => `${match} (i.e., ${year - parseInt(n, 10)})`)
121
+ .replace(/\ba few years ago\b/gi, `a few years ago (i.e., around ${year - 3})`)
122
+ .replace(/\blast week\b/gi, `last week (i.e., the week before ${anchor.dateStr})`)
123
+ .replace(/\blast saturday\b/gi, `last Saturday (i.e., the Saturday before ${anchor.dateStr})`);
124
+ }
125
+
126
+ // ---------------------------------------------------------------------------
127
+ // Renderer
128
+ // ---------------------------------------------------------------------------
129
+
130
+ /**
131
+ * Render recalled memories adapted to the current query context.
132
+ *
133
+ * - verbatim: return as-is in original order (default)
134
+ * - highlight: reorder by contextual relevance (60% vector score + 40% term overlap)
135
+ * - synthesize: highlight + temporal date annotation for "when" queries
136
+ */
137
+ export function renderMemories(
138
+ memories: RenderableMemory[],
139
+ query: string,
140
+ mode: RenderMode = "verbatim",
141
+ taskContext?: string,
142
+ ): RenderResult {
143
+ if (mode === "verbatim" || memories.length === 0) {
144
+ return {
145
+ mode: "verbatim",
146
+ memories: memories.map(m => ({
147
+ id: m.id,
148
+ text: m.text,
149
+ category: m.category,
150
+ relevance: m.score,
151
+ })),
152
+ };
153
+ }
154
+
155
+ const temporal = isTemporalQuery(query);
156
+ const queryTerms = extractTerms(query + (taskContext ? " " + taskContext : ""));
157
+
158
+ const scored = memories.map(m => {
159
+ const memTerms = extractTerms(m.text);
160
+ const overlap = computeTermOverlap(queryTerms, memTerms);
161
+ const relevance = 0.6 * m.score + 0.4 * overlap;
162
+
163
+ // In synthesize mode for temporal queries, annotate relative dates
164
+ const renderedText = (mode === "synthesize" && temporal)
165
+ ? resolveRelativeDates(m.text)
166
+ : m.text;
167
+
168
+ return {
169
+ id: m.id,
170
+ text: renderedText,
171
+ category: m.category,
172
+ relevance: Math.round(relevance * 1000) / 1000,
173
+ };
174
+ });
175
+
176
+ scored.sort((a, b) => b.relevance - a.relevance);
177
+
178
+ return { mode, memories: scored };
179
+ }