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
package/src/store.ts CHANGED
@@ -9,6 +9,7 @@ import { dirname } from "node:path";
9
9
  import { logWarn } from "./stderr-log.js";
10
10
  import type { DurableMemoryCategory } from "./memory-schema.js";
11
11
  import { matchesScopeFilter } from "./scope-policy.js";
12
+ import { detectEmotionIfEnabled } from "./emotion-detector.js";
12
13
 
13
14
  // ============================================================================
14
15
  // Types
@@ -42,6 +43,8 @@ export interface MemoryEntry {
42
43
  importance: number;
43
44
  timestamp: number;
44
45
  metadata?: string; // JSON string for extensible metadata — includes l0_abstract/l1_overview/l2_content/tier
46
+ language?: string; // ISO 639-1: "zh"|"ja"|"ko"|"en"
47
+ fts_text?: string; // Pre-tokenized text for FTS indexing
45
48
  }
46
49
 
47
50
  export interface MemorySearchResult {
@@ -258,14 +261,24 @@ export class MemoryStore {
258
261
  try {
259
262
  table = await db.openTable(TABLE_NAME);
260
263
 
261
- // Check if we need to add scope column for backward compatibility
264
+ // Backward compatibility: add missing columns to existing tables
262
265
  try {
263
266
  const sample = await table.query().limit(1).toArray();
264
- if (sample.length > 0 && !("scope" in sample[0])) {
265
- logWarn("Adding scope column for backward compatibility with existing data");
267
+ if (sample.length > 0) {
268
+ if (!("scope" in sample[0])) {
269
+ logWarn("Adding scope column for backward compatibility with existing data");
270
+ }
271
+ if (!("language" in sample[0])) {
272
+ logWarn("Adding language column for backward compatibility");
273
+ await table.addColumns([{ name: "language", valueSql: "'en'" }]);
274
+ }
275
+ if (!("fts_text" in sample[0])) {
276
+ logWarn("Adding fts_text column for backward compatibility");
277
+ await table.addColumns([{ name: "fts_text", valueSql: "text" }]);
278
+ }
266
279
  }
267
280
  } catch (err) {
268
- logWarn("Could not check table schema:", err);
281
+ logWarn("Could not check/migrate table schema:", err);
269
282
  }
270
283
  } catch (_openErr) {
271
284
  // Table doesn't exist yet — create it
@@ -278,6 +291,8 @@ export class MemoryStore {
278
291
  importance: 0,
279
292
  timestamp: 0,
280
293
  metadata: "{}",
294
+ language: "en",
295
+ fts_text: "__schema__",
281
296
  };
282
297
 
283
298
  try {
@@ -325,13 +340,13 @@ export class MemoryStore {
325
340
  // Check if FTS index already exists
326
341
  const indices = await table.listIndices();
327
342
  const hasFtsIndex = indices?.some((idx: any) =>
328
- idx.indexType === "FTS" || idx.columns?.includes("text")
343
+ idx.indexType === "FTS" || idx.columns?.includes("fts_text")
329
344
  );
330
345
 
331
346
  if (!hasFtsIndex) {
332
347
  // LanceDB @lancedb/lancedb >=0.26: use Index.fts() config
333
348
  const lancedb = await loadLanceDB();
334
- await table.createIndex("text", {
349
+ await table.createIndex("fts_text", {
335
350
  config: (lancedb as any).Index.fts(),
336
351
  });
337
352
  }
@@ -348,8 +363,17 @@ export class MemoryStore {
348
363
  id: entry.id || deterministicId(entry.scope, entry.text),
349
364
  timestamp: Date.now(),
350
365
  metadata: entry.metadata || "{}",
366
+ language: entry.language || "en",
367
+ fts_text: entry.fts_text || entry.text,
351
368
  };
352
369
 
370
+ const emotionResult = detectEmotionIfEnabled(fullEntry.text);
371
+ if (emotionResult) {
372
+ const existingMeta = JSON.parse(fullEntry.metadata || "{}");
373
+ existingMeta.emotion = emotionResult;
374
+ fullEntry.metadata = JSON.stringify(existingMeta);
375
+ }
376
+
353
377
  try {
354
378
  await this.table!.add([fullEntry]);
355
379
  } catch (err: any) {
@@ -375,10 +399,22 @@ export class MemoryStore {
375
399
  id: deterministicId(entry.scope, entry.text),
376
400
  timestamp: Date.now(),
377
401
  metadata: entry.metadata || "{}",
402
+ language: entry.language || "en",
403
+ fts_text: entry.fts_text || entry.text,
378
404
  }));
379
405
 
406
+ const enrichedEntries = fullEntries.map(e => {
407
+ const emotionResult = detectEmotionIfEnabled(e.text);
408
+ if (emotionResult) {
409
+ const meta = JSON.parse(e.metadata || "{}");
410
+ meta.emotion = emotionResult;
411
+ return { ...e, metadata: JSON.stringify(meta) };
412
+ }
413
+ return e;
414
+ });
415
+
380
416
  try {
381
- await this.table!.add(fullEntries);
417
+ await this.table!.add(enrichedEntries);
382
418
  } catch (err: any) {
383
419
  const code = err.code || "";
384
420
  const message = err.message || String(err);
@@ -414,8 +450,22 @@ export class MemoryStore {
414
450
  importance: Number.isFinite(entry.importance) ? entry.importance : 0.5,
415
451
  timestamp: Number.isFinite(entry.timestamp) ? entry.timestamp : Date.now(),
416
452
  metadata: entry.metadata || "{}",
453
+ language: entry.language || "en",
454
+ fts_text: entry.fts_text || entry.text,
417
455
  };
418
456
 
457
+ // HP-emo: Backfill emotion if absent during import
458
+ try {
459
+ const metaParsed = JSON.parse(full.metadata || "{}");
460
+ if (!metaParsed.emotion) {
461
+ const emotionResult = detectEmotionIfEnabled(full.text);
462
+ if (emotionResult) {
463
+ metaParsed.emotion = emotionResult;
464
+ full.metadata = JSON.stringify(metaParsed);
465
+ }
466
+ }
467
+ } catch { /* malformed metadata — skip backfill */ }
468
+
419
469
  await this.table!.add([full]);
420
470
  return full;
421
471
  }
@@ -716,6 +766,30 @@ export class MemoryStore {
716
766
  };
717
767
  }
718
768
 
769
+ /**
770
+ * Batch-fetch vectors for a list of entry IDs.
771
+ * Used by graph-export for cross-scope semantic bridge computation.
772
+ * Single LanceDB query — much faster than N individual getById calls.
773
+ */
774
+ async getVectors(ids: string[]): Promise<Map<string, number[]>> {
775
+ await this.ensureInitialized();
776
+ if (ids.length === 0) return new Map();
777
+
778
+ const conditions = ids.map(id => `id = '${escapeSqlLiteral(id)}'`).join(" OR ");
779
+ const rows = await this.table!.query()
780
+ .select(["id", "vector"])
781
+ .where(conditions)
782
+ .limit(ids.length)
783
+ .toArray();
784
+
785
+ const result = new Map<string, number[]>();
786
+ for (const row of rows) {
787
+ const vec = Array.from(row.vector as Iterable<number>);
788
+ if (vec.length > 0) result.set(row.id as string, vec);
789
+ }
790
+ return result;
791
+ }
792
+
719
793
  async stats(scopeFilter?: string[]): Promise<{
720
794
  totalCount: number;
721
795
  scopeCounts: Record<string, number>;
@@ -800,7 +874,7 @@ export class MemoryStore {
800
874
 
801
875
  async update(
802
876
  id: string,
803
- updates: { text?: string; vector?: number[]; importance?: number; category?: MemoryEntry["category"]; metadata?: string; timestamp?: number },
877
+ updates: { text?: string; vector?: number[]; importance?: number; category?: MemoryEntry["category"]; metadata?: string; timestamp?: number; language?: string; fts_text?: string },
804
878
  scopeFilter?: string[]
805
879
  ): Promise<MemoryEntry | null> {
806
880
  await this.ensureInitialized();
@@ -821,7 +895,7 @@ export class MemoryStore {
821
895
  rows = await this.table!.query().where(`id = '${safeId}'`).limit(1).toArray();
822
896
  } else {
823
897
  // Prefix match
824
- const all = await this.table!.query().select(["id", "text", "vector", "category", "scope", "importance", "timestamp", "metadata"]).toArray();
898
+ const all = await this.table!.query().select(["id", "text", "vector", "category", "scope", "importance", "timestamp", "metadata", "language", "fts_text"]).toArray();
825
899
  rows = all.filter((r: any) => (r.id as string).startsWith(id));
826
900
  if (rows.length > 1) {
827
901
  throw new Error(`Ambiguous prefix "${id}" matches ${rows.length} memories. Use a longer prefix or full ID.`);
@@ -838,7 +912,7 @@ export class MemoryStore {
838
912
  throw new Error(`Memory ${id} is outside accessible scopes`);
839
913
  }
840
914
 
841
- // Build updated entry, preserving original timestamp
915
+ // Build updated entry, preserving original timestamp and language fields
842
916
  const updated: MemoryEntry = {
843
917
  id: row.id as string,
844
918
  text: updates.text ?? (row.text as string),
@@ -848,8 +922,20 @@ export class MemoryStore {
848
922
  importance: updates.importance ?? Number(row.importance),
849
923
  timestamp: updates.timestamp ?? Number(row.timestamp),
850
924
  metadata: updates.metadata ?? ((row.metadata as string) || "{}"),
925
+ language: updates.language ?? ((row.language as string) || "en"),
926
+ fts_text: updates.fts_text ?? ((row.fts_text as string) || (updates.text ?? (row.text as string))),
851
927
  };
852
928
 
929
+ // HP-emo: Re-detect emotion when text changes
930
+ if (updates.text) {
931
+ const emotionResult = detectEmotionIfEnabled(updates.text);
932
+ if (emotionResult) {
933
+ const meta = JSON.parse(updated.metadata || "{}");
934
+ meta.emotion = emotionResult;
935
+ updated.metadata = JSON.stringify(meta);
936
+ }
937
+ }
938
+
853
939
  // LanceDB doesn't support in-place update; delete + re-add
854
940
  const resolvedId = escapeSqlLiteral(row.id as string);
855
941
  await this.table!.delete(`id = '${resolvedId}'`);
@@ -0,0 +1,66 @@
1
+ /**
2
+ * MP-1: Topic Tag — scope 内主题分区
3
+ *
4
+ * Keyword-based topic detection for memory entries.
5
+ * Tags are stored in metadata and used as a pre-filter during retrieval.
6
+ */
7
+
8
+ const TOPIC_PATTERNS: Array<{ tag: string; patterns: RegExp[] }> = [
9
+ { tag: "auth", patterns: [/\b(auth|login|oauth|jwt|session.?token|credentials?|password|sso|saml)\b/i] },
10
+ { tag: "deploy", patterns: [/\b(deploy|ci[\s/]?cd|pipeline|release|rollout|staging|production|rollback)\b/i] },
11
+ { tag: "infra", patterns: [/\b(docker|kubernetes|k8s|terraform|nginx|server|cluster|load.?balanc|cdn|dns)\b/i] },
12
+ { tag: "testing", patterns: [/\b(test|spec|jest|vitest|bun.?test|coverage|assertion|mock|fixture|e2e)\b/i] },
13
+ { tag: "database", patterns: [/\b(database|sql|postgres|mysql|sqlite|lancedb|mongodb|migration|schema|index)\b/i] },
14
+ { tag: "api", patterns: [/\b(api|endpoint|rest|graphql|grpc|webhook|http|request|response|route)\b/i] },
15
+ { tag: "ui", patterns: [/\b(ui|frontend|react|vue|svelte|css|tailwind|component|layout|responsive)\b/i] },
16
+ { tag: "perf", patterns: [/\b(performance|latency|throughput|cache|optimization|bottleneck|profil|benchmark)\b/i] },
17
+ { tag: "security", patterns: [/\b(security|vulnerability|xss|csrf|injection|encrypt|certificate|tls|ssl)\b/i] },
18
+ { tag: "docs", patterns: [/\b(documentation|readme|changelog|tutorial|guide|specification|api.?doc)\b/i] },
19
+ { tag: "memory", patterns: [/\b(memory|recall|retriev|embed|vector|checkpoint|distill|ingest|lancedb)\b/i] },
20
+ { tag: "mcp", patterns: [/\b(mcp|model.?context.?protocol|tool.?registr|mcp.?server)\b/i] },
21
+ { tag: "llm", patterns: [/\b(llm|language.?model|prompt|token|context.?window|embedding|openai|anthropic|claude)\b/i] },
22
+ { tag: "config", patterns: [/\b(config|settings?|environment|env.?var|dotenv|feature.?flag)\b/i] },
23
+ { tag: "data", patterns: [/\b(data|dataset|csv|json|parsing|transform|etl|pipeline|ingest)\b/i] },
24
+ ];
25
+
26
+ const MAX_SCAN_LENGTH = 2000;
27
+
28
+ export function detectTopicTag(text: string): string | undefined {
29
+ const sample = text.slice(0, MAX_SCAN_LENGTH).toLowerCase();
30
+ let bestTag: string | undefined;
31
+ let bestCount = 0;
32
+
33
+ for (const { tag, patterns } of TOPIC_PATTERNS) {
34
+ let count = 0;
35
+ for (const pattern of patterns) {
36
+ const matches = sample.match(new RegExp(pattern.source, "gi"));
37
+ if (matches) count += matches.length;
38
+ }
39
+ if (count > bestCount) {
40
+ bestCount = count;
41
+ bestTag = tag;
42
+ }
43
+ }
44
+
45
+ return bestCount >= 1 ? bestTag : undefined;
46
+ }
47
+
48
+ export function extractTopicTag(metadata?: string): string | undefined {
49
+ if (!metadata) return undefined;
50
+ try {
51
+ const parsed = JSON.parse(metadata);
52
+ return typeof parsed.topicTag === "string" ? parsed.topicTag : undefined;
53
+ } catch {
54
+ return undefined;
55
+ }
56
+ }
57
+
58
+ export function injectTopicTag(metadata: string, topicTag: string): string {
59
+ try {
60
+ const parsed = JSON.parse(metadata);
61
+ parsed.topicTag = topicTag;
62
+ return JSON.stringify(parsed);
63
+ } catch {
64
+ return metadata;
65
+ }
66
+ }
package/src/tracker.ts CHANGED
@@ -5,7 +5,9 @@
5
5
  import { readFileSync, writeFileSync, existsSync } from "node:fs";
6
6
  import { resolve } from "node:path";
7
7
 
8
- const TRACKER_PATH = resolve(import.meta.dir, "../data/ingested-files.json");
8
+ import { metaDir } from "./compat.js";
9
+
10
+ const TRACKER_PATH = resolve(metaDir(import.meta), "../data/ingested-files.json");
9
11
 
10
12
  interface TrackerData {
11
13
  files: Record<string, { ingestedAt: string; size: number; chunks: number; mtimeMs?: number }>;
package/src/ui-server.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  import { existsSync, realpathSync } from "node:fs";
4
4
  import { join, resolve, sep } from "node:path";
5
5
 
6
+ import { metaDir } from "./compat.js";
6
7
  import type { RetrievalResult } from "./retriever.js";
7
8
  import type { MemoryStore } from "./store.js";
8
9
  import { distillResults, formatExplainResults, formatSearchResults, selectBriefSeedResults, summarizeResults } from "./memory-output.js";
@@ -83,8 +84,8 @@ function errorResponse(error: unknown): Response {
83
84
  function isAllowedArtifactPath(targetPath: string): boolean {
84
85
  try {
85
86
  const resolved = realpathSync(targetPath);
86
- const exportsDir = resolve(import.meta.dir, "../data/exports");
87
- const pinsDir = resolve(import.meta.dir, "../data/pins");
87
+ const exportsDir = resolve(metaDir(import.meta), "../data/exports");
88
+ const pinsDir = resolve(metaDir(import.meta), "../data/pins");
88
89
  return (
89
90
  resolved === exportsDir ||
90
91
  resolved.startsWith(`${exportsDir}${sep}`) ||
@@ -141,7 +142,7 @@ function serializeResults(results: RetrievalResult[]) {
141
142
  }
142
143
 
143
144
  function serveStatic(pathname: string): Response | null {
144
- const uiDir = resolve(import.meta.dir, "../assets/ui");
145
+ const uiDir = resolve(metaDir(import.meta), "../assets/ui");
145
146
  const relativePath = pathname === "/"
146
147
  ? join(uiDir, "index.html")
147
148
  : join(uiDir, pathname.replace(/^\/ui\//, ""));
@@ -157,14 +158,17 @@ function serveStatic(pathname: string): Response | null {
157
158
  async function handleSearch(mode: "search" | "explain" | "distill", body: Record<string, any>) {
158
159
  const query = requireString(body.query, "query");
159
160
  const { retriever, profile } = getComponents(readOptionalString(body.profile));
161
+ const explicitScope = readOptionalString(body.scope);
160
162
  const results = await retriever.retrieve(buildRetrievalContext({
161
163
  query,
162
164
  limit: readLimit(body.limit, 5, 1, 20),
163
- scope: readOptionalString(body.scope),
165
+ scope: explicitScope,
164
166
  sessionId: readOptionalString(body.sessionId),
165
- allScopes: body.allScopes === true,
167
+ allScopes: explicitScope ? false : true,
168
+ topicTag: readOptionalString(body.topicTag),
166
169
  }, {
167
170
  operation: `ui:${mode}`,
171
+ allowUnscoped: true,
168
172
  }));
169
173
  const context = { query, profile: profile.name };
170
174
  const output = mode === "explain"
@@ -260,6 +264,28 @@ const server = Bun.serve({
260
264
  });
261
265
  }
262
266
 
267
+ if (request.method === "GET" && url.pathname === "/api/skills") {
268
+ const { store, embedder } = getComponents();
269
+ const { retrieveSkills } = await import("./skill-engine.js");
270
+ const scope = url.searchParams.get("scope") || undefined;
271
+ const skills = await retrieveSkills(store, embedder, "all skills", scope, 20);
272
+ return Response.json({
273
+ items: skills.map(({ skill, score }) => ({
274
+ shortId: skill.id.slice(0, 8),
275
+ name: skill.name,
276
+ description: skill.description,
277
+ trigger: skill.triggerPattern,
278
+ type: skill.implementationType,
279
+ implementation: skill.implementation,
280
+ verification: skill.verification,
281
+ scope: skill.scope,
282
+ tags: skill.tags,
283
+ score: Math.round(score * 100),
284
+ storedAt: skill.storedAt,
285
+ })),
286
+ });
287
+ }
288
+
263
289
  if (request.method === "GET" && url.pathname === "/api/stats") {
264
290
  const { store } = getComponents();
265
291
  const stats = await store.stats();
@@ -296,7 +322,7 @@ const server = Bun.serve({
296
322
  const scopeSelection = resolveScopeSelection({
297
323
  scope: readOptionalString(body.scope),
298
324
  sessionId: readOptionalString(body.sessionId),
299
- allScopes: body.allScopes === true,
325
+ allScopes: readOptionalString(body.scope) ? false : true,
300
326
  operation: "ui:pin",
301
327
  });
302
328
  const entry = await store.get(memoryId, scopeSelection.scopeFilter);
@@ -327,7 +353,7 @@ const server = Bun.serve({
327
353
  limit: readLimit(body.limit, 8, 1, 20),
328
354
  scope: readOptionalString(body.scope),
329
355
  sessionId: readOptionalString(body.sessionId),
330
- allScopes: body.allScopes === true,
356
+ allScopes: readOptionalString(body.scope) ? false : true,
331
357
  }, {
332
358
  operation: "ui:brief",
333
359
  }));
@@ -377,7 +403,7 @@ const server = Bun.serve({
377
403
  limit: readLimit(body.limit, 8, 1, 20),
378
404
  scope: readOptionalString(body.scope),
379
405
  sessionId: readOptionalString(body.sessionId),
380
- allScopes: body.allScopes === true,
406
+ allScopes: readOptionalString(body.scope) ? false : true,
381
407
  }, {
382
408
  operation: "ui:export",
383
409
  }));
@@ -420,6 +446,52 @@ const server = Bun.serve({
420
446
  });
421
447
  }
422
448
 
449
+ // ----- Dashboard API -----
450
+ if (request.method === "GET" && url.pathname === "/api/dashboard-stats") {
451
+ const { store } = getComponents();
452
+ const stats = await store.stats();
453
+ const allEntries = await store.list(undefined, undefined, 10000, 0);
454
+
455
+ const now = Date.now();
456
+ const weekAgo = now - 7 * 24 * 60 * 60 * 1000;
457
+ const monthAgo = now - 30 * 24 * 60 * 60 * 1000;
458
+ const thisWeek = allEntries.filter(e => e.timestamp > weekAgo).length;
459
+ const thisMonth = allEntries.filter(e => e.timestamp > monthAgo).length;
460
+
461
+ return Response.json({
462
+ totalCount: stats.totalCount,
463
+ categoryCounts: stats.categoryCounts,
464
+ scopeCounts: stats.scopeCounts,
465
+ growth: { thisWeek, thisMonth },
466
+ });
467
+ }
468
+
469
+ if (request.method === "GET" && url.pathname === "/api/stale-memories") {
470
+ const { store } = getComponents();
471
+ const { runMemoryLint } = await import("./memory-lint.js");
472
+ const report = await runMemoryLint({ store });
473
+ const staleFindings = report.findings.filter(f => f.check === "stale");
474
+ return Response.json({
475
+ count: staleFindings.length,
476
+ items: staleFindings.slice(0, 20).map(f => ({
477
+ detail: f.detail,
478
+ memoryIds: f.memoryIds,
479
+ })),
480
+ });
481
+ }
482
+
483
+ if (request.method === "GET" && url.pathname === "/api/lint-summary") {
484
+ const { store } = getComponents();
485
+ const { runMemoryLint } = await import("./memory-lint.js");
486
+ const report = await runMemoryLint({ store });
487
+ return Response.json({
488
+ healthScore: report.healthScore,
489
+ totalScanned: report.totalScanned,
490
+ summary: report.summary,
491
+ timestamp: report.timestamp,
492
+ });
493
+ }
494
+
423
495
  return new Response("Not Found", { status: 404 });
424
496
  } catch (error) {
425
497
  return errorResponse(error);
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
2
2
  import { basename, join, resolve } from "node:path";
3
3
 
4
+ import { metaDir } from "./compat.js";
4
5
  import { buildLegacyScopeKeepReview, suppressesLegacyScopeIssue } from "./legacy-scope-review.js";
5
6
  import { classifyLegacyScope } from "./store.js";
6
7
 
@@ -66,7 +67,7 @@ export interface WorkflowObservationScopeReviewOptions {
66
67
  }
67
68
 
68
69
  function defaultDir(): string {
69
- return resolve(import.meta.dir, "../data/workflow-observations");
70
+ return resolve(metaDir(import.meta), "../data/workflow-observations");
70
71
  }
71
72
 
72
73
  function clip(text: string, maxLen = 72): string {
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
 
4
+ import { metaDir } from "./compat.js";
4
5
  import type { WorkflowObservationOutcome, WorkflowObservationRecord } from "./workflow-observation-schema.js";
5
6
  import { WorkflowObservationRecordSchema } from "./workflow-observation-schema.js";
6
7
 
@@ -26,7 +27,7 @@ function sortNewestFirst(records: WorkflowObservationRecord[]): WorkflowObservat
26
27
  }
27
28
 
28
29
  export class WorkflowObservationStore {
29
- constructor(private readonly dir = resolve(import.meta.dir, "../data/workflow-observations")) {}
30
+ constructor(private readonly dir = resolve(metaDir(import.meta), "../data/workflow-observations")) {}
30
31
 
31
32
  get dataDir(): string {
32
33
  return ensureDir(this.dir);
@@ -0,0 +1,126 @@
1
+ /**
2
+ * HP-2: Formation Review Pass — post-write verification.
3
+ * After storing a memory, verify: embedding exists, metadata complete, scope set.
4
+ * Non-blocking: failures are logged but never block the write path.
5
+ */
6
+
7
+ import type { MemoryStore, MemoryEntry } from "./store.js";
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Config
11
+ // ---------------------------------------------------------------------------
12
+
13
+ export interface WriteVerifierConfig {
14
+ /** Enable/disable verification (default: true) */
15
+ enabled: boolean;
16
+ /** Maximum time to wait for verification in ms (default: 2000) */
17
+ timeoutMs: number;
18
+ }
19
+
20
+ export const DEFAULT_VERIFIER_CONFIG: WriteVerifierConfig = {
21
+ enabled: true,
22
+ timeoutMs: 2000,
23
+ };
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Verification result
27
+ // ---------------------------------------------------------------------------
28
+
29
+ export type VerificationIssue =
30
+ | "missing_entry"
31
+ | "missing_vector"
32
+ | "missing_scope"
33
+ | "missing_importance"
34
+ | "empty_text";
35
+
36
+ export interface VerificationResult {
37
+ ok: boolean;
38
+ entryId: string;
39
+ issues: VerificationIssue[];
40
+ durationMs: number;
41
+ }
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Public API
45
+ // ---------------------------------------------------------------------------
46
+
47
+ /**
48
+ * Verify a just-written memory entry by reading it back.
49
+ * Returns issues found (empty = all good).
50
+ */
51
+ export async function verifyWrite(
52
+ store: Pick<MemoryStore, "get">,
53
+ entryId: string,
54
+ cfg?: Partial<WriteVerifierConfig>,
55
+ ): Promise<VerificationResult> {
56
+ const config = { ...DEFAULT_VERIFIER_CONFIG, ...cfg };
57
+ const start = Date.now();
58
+
59
+ if (!config.enabled) {
60
+ return { ok: true, entryId, issues: [], durationMs: 0 };
61
+ }
62
+
63
+ try {
64
+ const entry = await Promise.race([
65
+ store.get(entryId),
66
+ new Promise<null>((_, reject) =>
67
+ setTimeout(() => reject(new Error("timeout")), config.timeoutMs),
68
+ ),
69
+ ]) as MemoryEntry | null;
70
+
71
+ const issues = checkEntry(entry, entryId);
72
+ return {
73
+ ok: issues.length === 0,
74
+ entryId,
75
+ issues,
76
+ durationMs: Date.now() - start,
77
+ };
78
+ } catch {
79
+ return {
80
+ ok: false,
81
+ entryId,
82
+ issues: ["missing_entry"],
83
+ durationMs: Date.now() - start,
84
+ };
85
+ }
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Internal checks
90
+ // ---------------------------------------------------------------------------
91
+
92
+ function checkEntry(entry: MemoryEntry | null, id: string): VerificationIssue[] {
93
+ const issues: VerificationIssue[] = [];
94
+
95
+ if (!entry) {
96
+ issues.push("missing_entry");
97
+ return issues;
98
+ }
99
+
100
+ // Vector should exist (embedding succeeded)
101
+ if (!entry.vector || (Array.isArray(entry.vector) && entry.vector.length === 0)) {
102
+ issues.push("missing_vector");
103
+ }
104
+
105
+ // Text should not be empty
106
+ if (!entry.text || entry.text.trim().length === 0) {
107
+ issues.push("empty_text");
108
+ }
109
+
110
+ // Metadata should contain scope
111
+ if (entry.metadata) {
112
+ try {
113
+ const meta = JSON.parse(entry.metadata);
114
+ if (!meta.scope) issues.push("missing_scope");
115
+ if (typeof meta.importance !== "number") issues.push("missing_importance");
116
+ } catch {
117
+ issues.push("missing_scope");
118
+ issues.push("missing_importance");
119
+ }
120
+ } else {
121
+ issues.push("missing_scope");
122
+ issues.push("missing_importance");
123
+ }
124
+
125
+ return issues;
126
+ }
@@ -1,29 +0,0 @@
1
- # RecallNest Memory Plugin
2
-
3
- RecallNest is a local-first shared memory layer backed by LanceDB. It stores and recalls context across sessions, terminals, and agents (Claude Code, Codex, Gemini CLI).
4
-
5
- **Prerequisite**: Requires [Bun](https://bun.sh) runtime. On first start, dependencies install automatically.
6
-
7
- ## Tool Quick Reference
8
-
9
- | When | Tool | Purpose |
10
- |------|------|---------|
11
- | Session opens, user says continue/接着/上个窗口 | `resume_context` | Load stable context, latest checkpoint, recent cases |
12
- | Before answering about prior work / known facts | `search_memory` | Targeted fact/pattern recall (2-3 key nouns) |
13
- | After solving something significant | `store_memory` or `store_case` | Persist durable knowledge |
14
- | Before closing a window with unfinished work | `checkpoint_session` | Save current state for next session |
15
- | After discovering a reusable workflow | `store_workflow_pattern` | Persist as durable pattern |
16
- | Memory feels stale or conflicted | `consolidate_memories` | Merge near-duplicates (dry-run by default) |
17
-
18
- ## Continuity Rules
19
-
20
- - If the user says `continue`, `继续`, `接着`, `刚才`, `上个窗口`, `不要让我重复前情`, or asks where you stopped / what to do next — call `resume_context` before any Bash, Read, or repo exploration.
21
- - If the user starts a concrete task in an active project or asks for prior implementation detail — do lightweight RecallNest recovery before repo exploration even without an explicit `continue`.
22
- - Do not substitute `git status`, `git log`, or reading local docs for `resume_context`. Local inspection can validate current code state only **after** continuity has been recovered.
23
- - When `scope` or `sessionId` is known, pass it into recall tools instead of relying on global recall.
24
- - If `resume_context` returns a resolved scope or checkpoint scope, reuse that same scope in follow-up `search_memory`, `brief_memory`, `pin_memory` calls.
25
- - If recalled context mentions repo state (modified files, pending push), treat it as **handoff context only** — do not restate as current state without verifying in this window.
26
- - Before leaving a window with resumable work, call `checkpoint_session`. Do not include repo state claims unless you actually inspected the repo this window.
27
- - Use `store_memory` for durable profile, preference, entity, or case knowledge.
28
- - Use `store_workflow_pattern` for reusable multi-step workflows.
29
- - If the user explicitly corrects a continuity miss, fix the workflow and call `workflow_observe` with `outcome: corrected`.
@@ -1,18 +0,0 @@
1
- {
2
- "name": "AliceLJY",
3
- "owner": {
4
- "name": "Alice Jin Ye"
5
- },
6
- "metadata": {
7
- "description": "RecallNest — shared memory layer for Claude Code, Codex, and Gemini CLI",
8
- "homepage": "https://github.com/AliceLJY/recallnest"
9
- },
10
- "plugins": [
11
- {
12
- "name": "recallnest",
13
- "version": "1.4.1",
14
- "source": "./",
15
- "description": "Local-first memory system: store, search, and recall context across sessions and terminals. Powered by LanceDB with hybrid retrieval."
16
- }
17
- ]
18
- }