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,475 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
2
+ import { randomUUID } from "node:crypto";
3
+ import { basename, extname, join, resolve } from "node:path";
4
+
5
+ import type { MemoryEntry } from "./store.js";
6
+ import type { RetrievalResult } from "./retriever.js";
7
+ import type { RetrievalProfileName } from "./retrieval-profiles.js";
8
+ import type { DistilledSummary } from "./memory-output.js";
9
+
10
+ export interface PinAsset {
11
+ id: string;
12
+ type: "pinned-memory";
13
+ createdAt: string;
14
+ updatedAt: string;
15
+ title: string;
16
+ summary: string;
17
+ tags: string[];
18
+ source: {
19
+ memoryId: string;
20
+ scope: string;
21
+ timestamp: number;
22
+ metadata: Record<string, unknown>;
23
+ };
24
+ retrieval?: {
25
+ query?: string;
26
+ profile?: RetrievalProfileName;
27
+ score?: number;
28
+ path?: string;
29
+ };
30
+ snippet: string;
31
+ }
32
+
33
+ export interface BriefAsset {
34
+ id: string;
35
+ type: "memory-brief";
36
+ createdAt: string;
37
+ updatedAt: string;
38
+ title: string;
39
+ summary: string;
40
+ tags: string[];
41
+ query: string;
42
+ profile: RetrievalProfileName;
43
+ hits: number;
44
+ sources: DistilledSummary["sources"];
45
+ takeaways: string[];
46
+ evidence: DistilledSummary["evidence"];
47
+ reusableCandidates: string[];
48
+ }
49
+
50
+ export type MemoryAsset = PinAsset | BriefAsset;
51
+ export type MemoryAssetRecord = MemoryAsset & { path: string };
52
+
53
+ export interface ExportArtifact {
54
+ id: string;
55
+ type: "memory-export";
56
+ query: string;
57
+ profile: RetrievalProfileName;
58
+ createdAt: string;
59
+ format: "md" | "json";
60
+ outputPath: string;
61
+ }
62
+
63
+ export interface ExportArtifactRecord extends ExportArtifact {
64
+ summary?: string;
65
+ path: string;
66
+ }
67
+
68
+ export interface DirtyBriefAssetRecord extends BriefAsset {
69
+ path: string;
70
+ reasons: string[];
71
+ scope: string;
72
+ }
73
+
74
+ const DATA_DIR = resolve(import.meta.dir, "../data");
75
+ const PINS_DIR = join(DATA_DIR, "pins");
76
+ const ASSETS_DIR = join(DATA_DIR, "assets");
77
+ const ARCHIVE_DIR = join(DATA_DIR, "archive", "dirty-briefs");
78
+ const EXPORTS_DIR = join(DATA_DIR, "exports");
79
+
80
+ function ensureDir(dir: string): string {
81
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
82
+ return dir;
83
+ }
84
+
85
+ function parseMetadata(entry: MemoryEntry): Record<string, unknown> {
86
+ try {
87
+ return JSON.parse(entry.metadata || "{}") as Record<string, unknown>;
88
+ } catch {
89
+ return {};
90
+ }
91
+ }
92
+
93
+ function cleanSnippet(text: string, maxLen = 180): string {
94
+ const compact = text.replace(/\s+/g, " ").trim();
95
+ if (compact.length <= maxLen) return compact;
96
+ return `${compact.slice(0, maxLen - 3)}...`;
97
+ }
98
+
99
+ function titleFromText(text: string): string {
100
+ const compact = text.replace(/\s+/g, " ").trim();
101
+ return compact.slice(0, 72) || "Pinned Memory";
102
+ }
103
+
104
+ function tagify(text: string): string[] {
105
+ const matches = text.match(/[\p{Script=Han}]{2,}|[a-z0-9._/-]{4,}/giu) || [];
106
+ return Array.from(new Set(matches.map(item => item.toLowerCase()))).slice(0, 6);
107
+ }
108
+
109
+ function retrievalPath(result: RetrievalResult): string {
110
+ const parts: string[] = [];
111
+ if (result.sources.vector) parts.push("vector");
112
+ if (result.sources.bm25) parts.push("bm25");
113
+ if (result.sources.reranked) parts.push("reranked");
114
+ return parts.join("+") || "direct";
115
+ }
116
+
117
+ export function getPinsDir(): string {
118
+ return ensureDir(PINS_DIR);
119
+ }
120
+
121
+ export function getAssetsDir(): string {
122
+ return ensureDir(ASSETS_DIR);
123
+ }
124
+
125
+ export function getDirtyBriefArchiveDir(): string {
126
+ return ensureDir(ARCHIVE_DIR);
127
+ }
128
+
129
+ export function getExportsDir(): string {
130
+ return ensureDir(EXPORTS_DIR);
131
+ }
132
+
133
+ export function buildPinAsset(result: RetrievalResult, options: {
134
+ title?: string;
135
+ summary?: string;
136
+ query?: string;
137
+ profile?: RetrievalProfileName;
138
+ } = {}): PinAsset {
139
+ const metadata = parseMetadata(result.entry);
140
+ const now = new Date().toISOString();
141
+ return {
142
+ id: randomUUID(),
143
+ type: "pinned-memory",
144
+ createdAt: now,
145
+ updatedAt: now,
146
+ title: options.title || titleFromText(result.entry.text),
147
+ summary: options.summary || cleanSnippet(result.entry.text, 240),
148
+ tags: tagify(`${options.query || ""} ${result.entry.scope} ${result.entry.text}`),
149
+ source: {
150
+ memoryId: result.entry.id,
151
+ scope: result.entry.scope,
152
+ timestamp: result.entry.timestamp,
153
+ metadata,
154
+ },
155
+ retrieval: {
156
+ query: options.query,
157
+ profile: options.profile,
158
+ score: result.score,
159
+ path: retrievalPath(result),
160
+ },
161
+ snippet: cleanSnippet(result.entry.text, 320),
162
+ };
163
+ }
164
+
165
+ export function savePinAsset(asset: PinAsset): string {
166
+ const dir = getPinsDir();
167
+ const path = join(dir, `${asset.id}.json`);
168
+ writeFileSync(path, JSON.stringify(asset, null, 2) + "\n");
169
+ return path;
170
+ }
171
+
172
+ export function buildBriefAsset(
173
+ summary: DistilledSummary,
174
+ options: { title?: string } = {},
175
+ ): BriefAsset {
176
+ const now = new Date().toISOString();
177
+ const takeaways = summary.takeaways.slice(0, 4);
178
+ const fallbackSummary = takeaways.map((item) => item.replace(/^[^:]+:\s*/, "")).join(" ").trim();
179
+
180
+ return {
181
+ id: randomUUID(),
182
+ type: "memory-brief",
183
+ createdAt: now,
184
+ updatedAt: now,
185
+ title: options.title || `${summary.query} brief`,
186
+ summary: cleanSnippet(fallbackSummary || `Brief for ${summary.query}`, 260),
187
+ tags: tagify(`${summary.query} ${takeaways.join(" ")} ${summary.sources.map((item) => item.source).join(" ")}`),
188
+ query: summary.query,
189
+ profile: summary.profile,
190
+ hits: summary.hits,
191
+ sources: summary.sources,
192
+ takeaways,
193
+ evidence: summary.evidence.slice(0, 6),
194
+ reusableCandidates: summary.reusableCandidates.slice(0, 4),
195
+ };
196
+ }
197
+
198
+ export function saveBriefAsset(asset: BriefAsset): string {
199
+ const dir = getAssetsDir();
200
+ const path = join(dir, `${asset.id}.json`);
201
+ writeFileSync(path, JSON.stringify(asset, null, 2) + "\n");
202
+ return path;
203
+ }
204
+
205
+ export function listPinAssets(limit = 20): Array<PinAsset & { path: string }> {
206
+ const dir = getPinsDir();
207
+ const files = readdirSync(dir)
208
+ .filter(name => name.endsWith(".json"))
209
+ .map(name => join(dir, name))
210
+ .sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)
211
+ .slice(0, limit);
212
+
213
+ const items: Array<PinAsset & { path: string }> = [];
214
+ for (const path of files) {
215
+ try {
216
+ const parsed = JSON.parse(readFileSync(path, "utf-8")) as PinAsset;
217
+ items.push({ ...parsed, path });
218
+ } catch {
219
+ // Skip corrupt asset files.
220
+ }
221
+ }
222
+ return items;
223
+ }
224
+
225
+ function listJsonRecords<T extends MemoryAsset>(dir: string, limit = 20): Array<T & { path: string }> {
226
+ ensureDir(dir);
227
+ const files = readdirSync(dir)
228
+ .filter(name => name.endsWith(".json"))
229
+ .map(name => join(dir, name))
230
+ .sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)
231
+ .slice(0, limit);
232
+
233
+ const items: Array<T & { path: string }> = [];
234
+ for (const path of files) {
235
+ try {
236
+ const parsed = JSON.parse(readFileSync(path, "utf-8")) as T;
237
+ items.push({ ...parsed, path });
238
+ } catch {
239
+ // Skip corrupt records.
240
+ }
241
+ }
242
+ return items;
243
+ }
244
+
245
+ export function listBriefAssets(limit = 20): Array<BriefAsset & { path: string }> {
246
+ return listJsonRecords<BriefAsset>(getAssetsDir(), limit);
247
+ }
248
+
249
+ function detectDirtyBriefReasons(asset: BriefAsset): string[] {
250
+ const reasons: string[] = [];
251
+ if (asset.sources.some((item) => item.source === "asset")) {
252
+ reasons.push("source includes asset");
253
+ }
254
+ if (asset.takeaways.some((item) => item.startsWith("asset:"))) {
255
+ reasons.push("takeaways include asset");
256
+ }
257
+ if (asset.evidence.some((item) => item.source === "asset")) {
258
+ reasons.push("evidence includes asset");
259
+ }
260
+ if (/\[(Pinned Asset|Memory Brief)\]/i.test(asset.summary)) {
261
+ reasons.push("summary contains asset marker");
262
+ }
263
+ if (asset.reusableCandidates.some((item) => /\[(Pinned Asset|Memory Brief)\]/i.test(item))) {
264
+ reasons.push("reusable candidate contains asset marker");
265
+ }
266
+ return reasons;
267
+ }
268
+
269
+ export function listDirtyBriefAssets(limit = 200): DirtyBriefAssetRecord[] {
270
+ return listBriefAssets(limit)
271
+ .map((asset) => {
272
+ const reasons = detectDirtyBriefReasons(asset);
273
+ return {
274
+ ...asset,
275
+ reasons,
276
+ scope: `asset:brief:${asset.id.slice(0, 8)}`,
277
+ };
278
+ })
279
+ .filter((asset) => asset.reasons.length > 0);
280
+ }
281
+
282
+ export function archiveDirtyBriefAsset(asset: DirtyBriefAssetRecord): string {
283
+ const dir = getDirtyBriefArchiveDir();
284
+ const targetPath = join(dir, basename(asset.path));
285
+ renameSync(asset.path, targetPath);
286
+ return targetPath;
287
+ }
288
+
289
+ export function listMemoryAssets(limit = 20): MemoryAssetRecord[] {
290
+ const pins = listPinAssets(limit);
291
+ const briefs = listBriefAssets(limit);
292
+ return [...pins, ...briefs]
293
+ .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
294
+ .slice(0, limit);
295
+ }
296
+
297
+ export function writeExportArtifact(params: {
298
+ query: string;
299
+ profile: RetrievalProfileName;
300
+ results: RetrievalResult[];
301
+ summary: string;
302
+ format: "md" | "json";
303
+ }): ExportArtifact {
304
+ const dir = getExportsDir();
305
+ const id = randomUUID();
306
+ const timestamp = new Date().toISOString();
307
+ const dateSlug = timestamp.replace(/[:]/g, "-").replace(/\..+/, "");
308
+ const safeStem = params.query
309
+ .toLowerCase()
310
+ .replace(/[^a-z0-9\p{Script=Han}]+/giu, "-")
311
+ .replace(/^-+|-+$/g, "")
312
+ .slice(0, 48) || "memory-export";
313
+ const filename = `${dateSlug}-${safeStem}.${params.format}`;
314
+ const outputPath = join(dir, filename);
315
+
316
+ const payload = {
317
+ id,
318
+ type: "memory-export",
319
+ query: params.query,
320
+ profile: params.profile,
321
+ createdAt: timestamp,
322
+ summary: params.summary,
323
+ results: params.results.map(result => ({
324
+ id: result.entry.id,
325
+ scope: result.entry.scope,
326
+ score: result.score,
327
+ timestamp: result.entry.timestamp,
328
+ text: result.entry.text,
329
+ metadata: parseMetadata(result.entry),
330
+ retrievalPath: retrievalPath(result),
331
+ })),
332
+ };
333
+
334
+ if (params.format === "json") {
335
+ writeFileSync(outputPath, JSON.stringify(payload, null, 2) + "\n");
336
+ } else {
337
+ const lines = [
338
+ `# Memory Export`,
339
+ "",
340
+ `- Query: ${params.query}`,
341
+ `- Profile: ${params.profile}`,
342
+ `- Created: ${timestamp}`,
343
+ `- Hits: ${params.results.length}`,
344
+ "",
345
+ `## Distilled Brief`,
346
+ "",
347
+ params.summary,
348
+ "",
349
+ `## Evidence`,
350
+ "",
351
+ ];
352
+
353
+ params.results.forEach((result, index) => {
354
+ const metadata = parseMetadata(result.entry);
355
+ lines.push(`### ${index + 1}. ${titleFromText(result.entry.text)}`);
356
+ lines.push("");
357
+ lines.push(`- Memory ID: ${result.entry.id}`);
358
+ lines.push(`- Scope: ${result.entry.scope}`);
359
+ lines.push(`- Score: ${(result.score * 100).toFixed(0)}%`);
360
+ lines.push(`- File: ${String(metadata.file || metadata.heading || "-")}`);
361
+ lines.push(`- Date: ${new Date(result.entry.timestamp).toISOString()}`);
362
+ lines.push(`- Retrieval: ${retrievalPath(result)}`);
363
+ lines.push("");
364
+ lines.push(result.entry.text);
365
+ lines.push("");
366
+ });
367
+
368
+ writeFileSync(outputPath, lines.join("\n"));
369
+ }
370
+
371
+ return {
372
+ id,
373
+ type: "memory-export",
374
+ query: params.query,
375
+ profile: params.profile,
376
+ createdAt: timestamp,
377
+ format: params.format,
378
+ outputPath,
379
+ };
380
+ }
381
+
382
+ export function readPinAsset(pinIdOrFile: string): (PinAsset & { path: string }) | null {
383
+ const dir = getPinsDir();
384
+ const directPath = pinIdOrFile.endsWith(".json") ? pinIdOrFile : join(dir, `${pinIdOrFile}.json`);
385
+ const candidates = existsSync(directPath)
386
+ ? [directPath]
387
+ : readdirSync(dir)
388
+ .filter(name => name.endsWith(".json") && basename(name, ".json").startsWith(pinIdOrFile))
389
+ .map(name => join(dir, name));
390
+
391
+ if (candidates.length === 0) return null;
392
+ if (candidates.length > 1) {
393
+ throw new Error(`Ambiguous pin id prefix: ${pinIdOrFile}`);
394
+ }
395
+
396
+ const path = candidates[0];
397
+ const parsed = JSON.parse(readFileSync(path, "utf-8")) as PinAsset;
398
+ return { ...parsed, path };
399
+ }
400
+
401
+ export function assetSummaryLine(asset: MemoryAsset): string {
402
+ const scope = asset.type === "pinned-memory"
403
+ ? asset.source.scope
404
+ : asset.sources.map((item) => item.source).join(", ").slice(0, 24) || "brief";
405
+ const kind = asset.type === "pinned-memory" ? "pin" : "brief";
406
+ return `${asset.id.slice(0, 8)} ${kind.padEnd(5)} ${asset.title} [${scope}] ${asset.createdAt.slice(0, 10)}`;
407
+ }
408
+
409
+ export function pinSummaryLine(asset: PinAsset): string {
410
+ return assetSummaryLine(asset);
411
+ }
412
+
413
+ function parseMarkdownExport(path: string): ExportArtifactRecord | null {
414
+ const content = readFileSync(path, "utf-8");
415
+ const lines = content.split("\n");
416
+ const query = lines.find(line => line.startsWith("- Query: "))?.replace("- Query: ", "").trim() || "";
417
+ const profile = (lines.find(line => line.startsWith("- Profile: "))?.replace("- Profile: ", "").trim() || "default") as RetrievalProfileName;
418
+ const createdAt = lines.find(line => line.startsWith("- Created: "))?.replace("- Created: ", "").trim() || new Date(statSync(path).mtimeMs).toISOString();
419
+ const briefIndex = lines.findIndex(line => line.trim() === "## Distilled Brief");
420
+ let summary = "";
421
+ if (briefIndex >= 0) {
422
+ summary = lines.slice(briefIndex + 2, briefIndex + 12).join("\n").trim();
423
+ }
424
+
425
+ return {
426
+ id: basename(path, extname(path)),
427
+ type: "memory-export",
428
+ query,
429
+ profile,
430
+ createdAt,
431
+ format: "md",
432
+ outputPath: path,
433
+ summary,
434
+ path,
435
+ };
436
+ }
437
+
438
+ function parseJsonExport(path: string): ExportArtifactRecord | null {
439
+ try {
440
+ const parsed = JSON.parse(readFileSync(path, "utf-8")) as Record<string, unknown>;
441
+ return {
442
+ id: String(parsed.id || basename(path, extname(path))),
443
+ type: "memory-export",
444
+ query: String(parsed.query || ""),
445
+ profile: String(parsed.profile || "default") as RetrievalProfileName,
446
+ createdAt: String(parsed.createdAt || new Date(statSync(path).mtimeMs).toISOString()),
447
+ format: "json",
448
+ outputPath: path,
449
+ summary: typeof parsed.summary === "string" ? parsed.summary : "",
450
+ path,
451
+ };
452
+ } catch {
453
+ return null;
454
+ }
455
+ }
456
+
457
+ export function listExportArtifacts(limit = 20): ExportArtifactRecord[] {
458
+ const dir = getExportsDir();
459
+ const files = readdirSync(dir)
460
+ .filter(name => name.endsWith(".md") || name.endsWith(".json"))
461
+ .map(name => join(dir, name))
462
+ .sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)
463
+ .slice(0, limit);
464
+
465
+ const items: ExportArtifactRecord[] = [];
466
+ for (const path of files) {
467
+ const parsed = path.endsWith(".json")
468
+ ? parseJsonExport(path)
469
+ : parseMarkdownExport(path);
470
+ if (parsed) {
471
+ items.push(parsed);
472
+ }
473
+ }
474
+ return items;
475
+ }