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
@@ -1,4 +1,13 @@
1
1
  import type { Embedder } from "./embedder.js";
2
+ import { detectLang, tokenizeFts } from "./language-hook.js";
3
+ import { generateAnchor } from "./anchor-generator.js";
4
+ import { incrementWriteCount } from "./activity-counter.js";
5
+ import { verifyWrite } from "./write-verifier.js";
6
+ // batchInternalDedup is available in ingest.ts for large-batch scenarios;
7
+ // persistMemoryBatch relies on per-item conflict detection (A-2) instead.
8
+ import { defaultEvolution, buildSupersedeMetadata, buildSupersedeMetadataForNew, buildPendingReviewMetadata, isActiveMemory, patchEvolution } from "./memory-evolution.js";
9
+ import { detectTopicTag, injectTopicTag } from "./topic-tag.js";
10
+ import { assignDefaultConfidence, type ConfidenceMetadata } from "./confidence-tracker.js";
2
11
  import {
3
12
  type CaseMemoryInput,
4
13
  CaseMemoryInputSchema,
@@ -17,6 +26,7 @@ import {
17
26
  type DurableMemoryCategory,
18
27
  DURABLE_MEMORY_CATEGORIES,
19
28
  type WriteDisposition,
29
+ parsePrivacyTier,
20
30
  } from "./memory-schema.js";
21
31
  import { deterministicId, type MemoryEntry, type MemoryStore } from "./store.js";
22
32
  import {
@@ -36,6 +46,7 @@ import { buildConflictCandidateRecord, buildConflictFingerprint, reopenConflictC
36
46
  import type { ConflictCandidateStore } from "./conflict-store.js";
37
47
  import {
38
48
  inferPreferenceSlot,
49
+ inferImplicitUsageSlot,
39
50
  samePreferenceSlot,
40
51
  type AtomicBrandItemPreferenceSlot,
41
52
  type PreferenceSlot,
@@ -45,11 +56,15 @@ import {
45
56
  import { matchPreference, applyPreferenceMatch } from "./preference-matcher.js";
46
57
  import type { LLMClient } from "./llm-client.js";
47
58
  import type { KGExtractor } from "./kg-extractor.js";
59
+ import { scanForPII } from "./pii-detector.js";
60
+ import type { AuditLogger } from "./audit-log.js";
61
+ import { checkAdmission, type ScopeRateLimiter, type AdmissionConfig } from "./admission-control.js";
62
+ import { tagNarrativeIfEnabled } from "./narrative-tagger.js";
48
63
 
49
64
  type StoreDeps = Pick<MemoryStore, "store"> & Partial<Pick<MemoryStore, "list" | "update" | "getById" | "get" | "vectorSearch">>;
50
65
  type ConflictStoreDeps = Pick<ConflictCandidateStore, "save" | "replace" | "getOpenByFingerprint" | "getLatestByFingerprint">;
51
66
 
52
- interface PersistMemoryDeps {
67
+ export interface PersistMemoryDeps {
53
68
  store: StoreDeps;
54
69
  embedder: Pick<Embedder, "embedPassage">;
55
70
  conflictStore?: ConflictStoreDeps;
@@ -57,6 +72,12 @@ interface PersistMemoryDeps {
57
72
  llm?: LLMClient | null;
58
73
  /** Tier 4.1: Optional KG triple extractor (async, non-blocking) */
59
74
  kgExtractor?: KGExtractor | null;
75
+ /** F-1: Optional audit logger for recording store operations */
76
+ auditLogger?: AuditLogger | null;
77
+ /** LME-8: Optional rate limiter for admission control */
78
+ rateLimiter?: ScopeRateLimiter | null;
79
+ /** LME-8: Optional admission config overrides */
80
+ admissionConfig?: Partial<AdmissionConfig>;
60
81
  }
61
82
 
62
83
  interface DurableWriteInput {
@@ -71,6 +92,8 @@ interface DurableWriteInput {
71
92
  source: "manual" | "agent" | "api";
72
93
  sourceCategory?: string;
73
94
  sourceBoundary?: ReturnType<typeof extractBoundaryMetadata>;
95
+ language?: string;
96
+ fts_text?: string;
74
97
  }
75
98
 
76
99
  const CANONICAL_SCAN_LIMIT = 1000;
@@ -124,14 +147,28 @@ function buildStructuredMetadata(params: {
124
147
  category: DurableMemoryCategory;
125
148
  canonicalKey: string;
126
149
  extra?: Record<string, unknown>;
150
+ /** Optional: scope + text for narrative tagging (Phase 3) */
151
+ narrativeInput?: { scope: string; text: string; sessionId?: string };
127
152
  }): string {
153
+ // HP-narrative: Tag with autobiographical narrative metadata when enabled
154
+ const narrative = params.narrativeInput
155
+ ? tagNarrativeIfEnabled({
156
+ scope: params.narrativeInput.scope,
157
+ text: params.narrativeInput.text,
158
+ timestamp: Date.now(),
159
+ sessionId: params.narrativeInput.sessionId,
160
+ })
161
+ : null;
162
+
128
163
  return JSON.stringify({
129
164
  source: params.source,
130
165
  tags: params.tags,
131
166
  capture: params.capture,
132
167
  boundary: buildStructuredMemoryBoundary(params.category),
133
168
  canonicalKey: params.canonicalKey,
169
+ evolution: defaultEvolution(),
134
170
  ...params.extra,
171
+ ...(narrative ? { narrative } : {}),
135
172
  });
136
173
  }
137
174
 
@@ -533,6 +570,8 @@ async function writeDurableEntry(
533
570
  category: params.category,
534
571
  metadata: params.metadata,
535
572
  timestamp: Date.now(),
573
+ ...(params.language ? { language: params.language } : {}),
574
+ ...(params.fts_text ? { fts_text: params.fts_text } : {}),
536
575
  });
537
576
  if (!updated) {
538
577
  throw new Error(`Failed to update canonical memory ${latest.id}`);
@@ -553,6 +592,8 @@ async function writeDurableEntry(
553
592
  scope: params.scope,
554
593
  importance: params.importance,
555
594
  metadata: params.metadata,
595
+ ...(params.language ? { language: params.language } : {}),
596
+ ...(params.fts_text ? { fts_text: params.fts_text } : {}),
556
597
  });
557
598
 
558
599
  return {
@@ -599,13 +640,20 @@ function inferPromotedCategory(
599
640
  }
600
641
 
601
642
  function buildStoreMemoryMetadata(input: StoreMemoryInput, canonicalKey: string): string {
643
+ const anchor = generateAnchor(input.text);
644
+ const anchorExtra = anchor ? { anchor } : undefined;
645
+ // HP-ethics: Include privacyTier in metadata if non-default
646
+ const privacyExtra = input.privacyTier && input.privacyTier !== "durable"
647
+ ? { privacyTier: input.privacyTier }
648
+ : undefined;
602
649
  return buildStructuredMetadata({
603
650
  source: input.source,
604
651
  tags: input.tags,
605
652
  capture: "store_memory_schema_v1",
606
653
  category: input.category,
607
654
  canonicalKey,
608
- extra: buildPreferenceSlotExtra(input.category, input.text),
655
+ extra: mergeExtra(buildPreferenceSlotExtra(input.category, input.text), anchorExtra, privacyExtra),
656
+ narrativeInput: { scope: input.scope, text: input.text },
609
657
  });
610
658
  }
611
659
 
@@ -614,21 +662,25 @@ function buildWorkflowPatternMetadata(
614
662
  tags: string[],
615
663
  canonicalKey: string,
616
664
  ): string {
665
+ const wpExtra: Record<string, unknown> = {
666
+ workflowPattern: {
667
+ title: input.title,
668
+ trigger: input.trigger,
669
+ steps: input.steps,
670
+ outcome: input.outcome,
671
+ tools: input.tools,
672
+ },
673
+ };
674
+ const anchor = generateAnchor(input.title + ": " + input.trigger, wpExtra);
675
+ if (anchor) wpExtra.anchor = anchor;
617
676
  return buildStructuredMetadata({
618
677
  source: input.source,
619
678
  tags,
620
679
  capture: "workflow_pattern_schema_v1",
621
680
  category: "patterns",
622
681
  canonicalKey,
623
- extra: {
624
- workflowPattern: {
625
- title: input.title,
626
- trigger: input.trigger,
627
- steps: input.steps,
628
- outcome: input.outcome,
629
- tools: input.tools,
630
- },
631
- },
682
+ extra: wpExtra,
683
+ narrativeInput: { scope: input.scope, text: input.title + ": " + input.trigger },
632
684
  });
633
685
  }
634
686
 
@@ -637,22 +689,26 @@ function buildCaseMemoryMetadata(
637
689
  tags: string[],
638
690
  canonicalKey: string,
639
691
  ): string {
692
+ const cmExtra: Record<string, unknown> = {
693
+ caseMemory: {
694
+ title: input.title,
695
+ problem: input.problem,
696
+ context: input.context,
697
+ solutionSteps: input.solutionSteps,
698
+ outcome: input.outcome,
699
+ tools: input.tools,
700
+ },
701
+ };
702
+ const anchor = generateAnchor(input.title + ": " + input.problem, cmExtra);
703
+ if (anchor) cmExtra.anchor = anchor;
640
704
  return buildStructuredMetadata({
641
705
  source: input.source,
642
706
  tags,
643
707
  capture: "case_memory_schema_v1",
644
708
  category: "cases",
645
709
  canonicalKey,
646
- extra: {
647
- caseMemory: {
648
- title: input.title,
649
- problem: input.problem,
650
- context: input.context,
651
- solutionSteps: input.solutionSteps,
652
- outcome: input.outcome,
653
- tools: input.tools,
654
- },
655
- },
710
+ extra: cmExtra,
711
+ narrativeInput: { scope: input.scope, text: input.title + ": " + input.problem },
656
712
  });
657
713
  }
658
714
 
@@ -682,6 +738,7 @@ function buildPromotionMetadata(
682
738
  },
683
739
  },
684
740
  ),
741
+ narrativeInput: { scope: input.scope, text },
685
742
  });
686
743
  }
687
744
 
@@ -690,7 +747,77 @@ export async function persistMemory(
690
747
  rawInput: unknown,
691
748
  ): Promise<StoredMemoryRecord> {
692
749
  const input = StoreMemoryInputSchema.parse(rawInput);
750
+
751
+ // F3: Extract temporal validity params before Zod strips them
752
+ const rawObj = rawInput as Record<string, unknown> | null;
753
+ const validUntilRaw = rawObj?.validUntil;
754
+ const eventTimeRaw = rawObj?.eventTime;
755
+
756
+ // LC-P5: Large text gate — texts > 8000 chars get truncated to L0 summary.
757
+ // Prevents oversized entries from bloating the index and degrading embedding quality.
758
+ const LARGE_TEXT_THRESHOLD = 8000;
759
+ if (input.text.length > LARGE_TEXT_THRESHOLD && deps.llm) {
760
+ try {
761
+ const summary = await deps.llm.generateCoreSummary(input.text);
762
+ if (summary) {
763
+ // Store original length in metadata for reference, replace text with summary
764
+ const origLength = input.text.length;
765
+ input.text = summary;
766
+ input.tags = [...(input.tags ?? []), `truncated:${origLength}`];
767
+ }
768
+ } catch {
769
+ // Fallback: hard truncate if LLM fails
770
+ input.text = input.text.slice(0, LARGE_TEXT_THRESHOLD);
771
+ }
772
+ } else if (input.text.length > LARGE_TEXT_THRESHOLD) {
773
+ // No LLM available — hard truncate
774
+ input.text = input.text.slice(0, LARGE_TEXT_THRESHOLD);
775
+ }
776
+
777
+ // B-3: LLM importance assessment — refine default importance (0.7) via LLM.
778
+ // Only fires when importance is at the default value and LLM is available.
779
+ // Explicit importance values from the caller are respected as-is.
780
+ let pendingReview = false; // HP-6: track low-confidence assessment
781
+ if (input.importance === 0.7 && deps.llm) {
782
+ try {
783
+ const assessed = await deps.llm.assessImportance(input.text, input.category);
784
+ if (assessed !== null) {
785
+ input.importance = assessed;
786
+ // HP-6: Very low importance from LLM → pending review for distill to re-evaluate
787
+ if (assessed < 0.3) pendingReview = true;
788
+ }
789
+ } catch {
790
+ // LLM importance assessment must never block memory writes
791
+ }
792
+ }
793
+
794
+ // LME-8: Admission control — reject low-quality writes before expensive embedding
795
+ const admissionScope = input.scope || "unknown";
796
+ const admission = checkAdmission(
797
+ input.text,
798
+ input.importance,
799
+ admissionScope,
800
+ deps.rateLimiter ?? undefined,
801
+ deps.admissionConfig,
802
+ );
803
+ if (admission.verdict === "rejected") {
804
+ return {
805
+ id: `rejected-${Date.now()}`,
806
+ text: input.text,
807
+ category: input.category,
808
+ importance: input.importance,
809
+ scope: admissionScope,
810
+ source: input.source,
811
+ tags: input.tags ?? [],
812
+ resolvedScope: admissionScope,
813
+ storedAt: new Date().toISOString(),
814
+ disposition: "rejected" as const,
815
+ };
816
+ }
817
+
693
818
  const vector = await deps.embedder.embedPassage(input.text);
819
+ const language = detectLang(input.text);
820
+ const fts_text = tokenizeFts(input.text, language);
694
821
  const resolvedScope = resolveScope(input);
695
822
  const canonicalKey = resolveCanonicalKey({
696
823
  category: input.category,
@@ -738,24 +865,229 @@ export async function persistMemory(
738
865
  }
739
866
  }
740
867
 
868
+ // A-2: Evolution-aware semantic conflict detection.
869
+ // For non-preferences categories, check if a semantically similar memory
870
+ // already exists in the same scope. If so, use LLM to decide whether the
871
+ // old memory should be superseded.
872
+ // Requires both vectorSearch (for candidate lookup) and LLM (for judgment).
873
+ // Graceful fallback: if either is unavailable, skip detection entirely.
874
+ let supersededOldId: string | null = null; // HP-1: track for bidirectional link
875
+ let supersededReason: string | null = null;
876
+ if (
877
+ input.category !== "preferences" &&
878
+ deps.store.vectorSearch &&
879
+ deps.llm &&
880
+ deps.store.update
881
+ ) {
882
+ try {
883
+ const candidates = await deps.store.vectorSearch(vector, 3, 0.85, [resolvedScope]);
884
+ // Only consider same-category, active memories as supersede targets
885
+ const supersedeTargets = candidates.filter(
886
+ c => c.entry.category === input.category &&
887
+ c.entry.id !== deterministicId(resolvedScope, input.text) &&
888
+ isActiveMemory(c.entry.metadata),
889
+ );
890
+ if (supersedeTargets.length > 0) {
891
+ const decision = await deps.llm.dedupDecision(input.text, supersedeTargets[0].entry.text);
892
+ if (decision.action === "MERGE" || decision.action === "SKIP") {
893
+ // LLM says new info overlaps with existing → supersede the old one
894
+ // (MERGE = new has extra info → store new, mark old superseded)
895
+ // (SKIP = identical → just dedup, handled by writeDurableEntry)
896
+ if (decision.action === "MERGE") {
897
+ const oldEntry = supersedeTargets[0].entry;
898
+ const supersededMeta = buildSupersedeMetadata(oldEntry.metadata, deterministicId(resolvedScope, input.text));
899
+ await deps.store.update(oldEntry.id, { metadata: supersededMeta });
900
+ // HP-1: Track for bidirectional link
901
+ supersededOldId = oldEntry.id;
902
+ supersededReason = decision.reason;
903
+ }
904
+ }
905
+ // CREATE or error → proceed normally (store new memory as-is)
906
+ }
907
+ } catch {
908
+ // Evolution conflict detection must never block memory writes
909
+ }
910
+ }
911
+
912
+ // F2: Interference pre-warning — if scope already has ≥5 high-similarity memories,
913
+ // mark the weakest as pending_review and record an interferenceNote.
914
+ if (deps.store.vectorSearch && deps.store.update) {
915
+ try {
916
+ const nearMatches = await deps.store.vectorSearch(vector, 6, 0.82, [resolvedScope]);
917
+ const activeNear = nearMatches.filter(
918
+ c => c.entry.id !== deterministicId(resolvedScope, input.text) &&
919
+ isActiveMemory(c.entry.metadata),
920
+ );
921
+ if (activeNear.length >= 5) {
922
+ // Find weakest by importance × confidence
923
+ const weakest = activeNear.reduce((min, cur) => {
924
+ const curScore = (cur.entry.importance ?? 0.5) * cur.score;
925
+ const minScore = (min.entry.importance ?? 0.5) * min.score;
926
+ return curScore < minScore ? cur : min;
927
+ });
928
+ const reviewMeta = buildPendingReviewMetadata(weakest.entry.metadata);
929
+ const parsed: Record<string, unknown> = JSON.parse(reviewMeta);
930
+ const evo = parsed.evolution as Record<string, unknown> | undefined;
931
+ if (evo) evo.evolutionNote = `F2: interference cluster detected (${activeNear.length} similar active memories)`;
932
+ await deps.store.update(weakest.entry.id, { metadata: JSON.stringify(parsed) });
933
+ }
934
+ } catch {
935
+ // Interference detection must never block memory writes
936
+ }
937
+ }
938
+
939
+ // F-3: PII detection — inject warning into metadata if PII found.
940
+ // Non-blocking: PII does not prevent the write.
941
+ let metadata = buildStoreMemoryMetadata(input, canonicalKey);
942
+ const piiResult = scanForPII(input.text);
943
+ if (piiResult.hasPII) {
944
+ const parsed: Record<string, unknown> = JSON.parse(metadata);
945
+ parsed.piiWarning = {
946
+ summary: piiResult.summary,
947
+ detections: piiResult.detections.length,
948
+ severity: piiResult.detections.some(d => d.severity === "high") ? "high" : "medium",
949
+ };
950
+ metadata = JSON.stringify(parsed);
951
+ }
952
+
953
+ // MP-1: Topic Tag — auto-detect or use explicit tag from caller
954
+ const topicTag = (input as Record<string, unknown>).topicTag as string | undefined
955
+ ?? detectTopicTag(input.text);
956
+ if (topicTag) {
957
+ metadata = injectTopicTag(metadata, topicTag);
958
+ }
959
+
960
+ // HP-1: Bidirectional supersede link — mark new memory as superseding old
961
+ if (supersededOldId) {
962
+ metadata = buildSupersedeMetadataForNew(metadata, supersededOldId, supersededReason ?? undefined);
963
+ }
964
+
965
+ // HP-6: Low-confidence entries → pending_review (distill prioritizes these)
966
+ if (pendingReview) {
967
+ metadata = buildPendingReviewMetadata(metadata);
968
+ }
969
+
970
+ // F3: Temporal validity — inject validUntil and eventTime into evolution metadata
971
+ if (validUntilRaw != null || eventTimeRaw != null) {
972
+ const temporalPatch: Record<string, number> = {};
973
+ if (validUntilRaw != null) {
974
+ temporalPatch.validUntil = typeof validUntilRaw === "number"
975
+ ? validUntilRaw
976
+ : new Date(String(validUntilRaw)).getTime();
977
+ }
978
+ if (eventTimeRaw != null) {
979
+ temporalPatch.eventTime = typeof eventTimeRaw === "number"
980
+ ? eventTimeRaw
981
+ : new Date(String(eventTimeRaw)).getTime();
982
+ }
983
+ metadata = patchEvolution(metadata, temporalPatch);
984
+ }
985
+
986
+ // F1: Assign structured confidence based on source (user can override via explicit param)
987
+ const confidenceRaw = rawObj?.confidence as Partial<ConfidenceMetadata> | number | undefined;
988
+ const explicitConfidence = typeof confidenceRaw === "number"
989
+ ? { score: confidenceRaw }
990
+ : (confidenceRaw ?? undefined);
991
+ const confidenceMeta = assignDefaultConfidence(input.source, explicitConfidence);
992
+ {
993
+ const parsed: Record<string, unknown> = JSON.parse(metadata);
994
+ parsed.confidence = confidenceMeta;
995
+ metadata = JSON.stringify(parsed);
996
+ }
997
+
741
998
  const { entry, disposition, conflictId } = await writeDurableEntry(deps, {
742
999
  text: input.text,
743
1000
  vector,
744
1001
  category: input.category,
745
1002
  scope: resolvedScope,
746
1003
  importance: input.importance,
747
- metadata: buildStoreMemoryMetadata(input, canonicalKey),
1004
+ metadata,
748
1005
  canonicalKey,
749
1006
  source: input.source,
1007
+ language,
1008
+ fts_text,
750
1009
  });
751
1010
 
752
1011
  // Tier 4.1: Async KG triple extraction (non-blocking)
753
- if (deps.kgExtractor && disposition !== "deduped") {
1012
+ // HP-ethics: Skip KG extraction for ephemeral/private memories — they must not leave graph traces
1013
+ const entryPrivacyTier = parsePrivacyTier(metadata);
1014
+ const kgAllowed = entryPrivacyTier !== "ephemeral" && entryPrivacyTier !== "private";
1015
+ if (deps.kgExtractor && disposition !== "deduped" && kgAllowed) {
754
1016
  deps.kgExtractor
755
1017
  .extractAndStore(input.text, entry.id, resolvedScope)
756
1018
  .catch(() => {}); // Silently ignore — KG extraction must never block memory writes
757
1019
  }
758
1020
 
1021
+ // F-1: Audit log — record store operation (non-blocking, silent on failure)
1022
+ try {
1023
+ deps.auditLogger?.log({
1024
+ operation: "store",
1025
+ scope: resolvedScope,
1026
+ memoryId: entry.id,
1027
+ actor: input.source,
1028
+ details: `${input.category}: ${input.text.slice(0, 100)}`,
1029
+ });
1030
+ } catch {
1031
+ // Audit must never block memory writes
1032
+ }
1033
+
1034
+ // HP-3: Activity counter — track non-dedup writes for distill trigger
1035
+ if (disposition !== "deduped") {
1036
+ try {
1037
+ incrementWriteCount();
1038
+ } catch {
1039
+ // Activity tracking must never block memory writes
1040
+ }
1041
+ }
1042
+
1043
+ // HP-2: Post-write verification — async, non-blocking
1044
+ if (disposition !== "deduped" && deps.store.get) {
1045
+ verifyWrite(deps.store as Pick<import("./store.js").MemoryStore, "get">, entry.id)
1046
+ .then(result => {
1047
+ if (!result.ok) {
1048
+ console.error(
1049
+ `[HP-2] Write verification issues for ${entry.id}: ${result.issues.join(", ")} (${result.durationMs}ms)`,
1050
+ );
1051
+ }
1052
+ })
1053
+ .catch(() => {}); // Must never block
1054
+ }
1055
+
1056
+ // LME-1: Implicit preference dual-write — when a non-preference memory
1057
+ // contains an implicit usage signal ("I use X", "I have X", etc.),
1058
+ // store an additional preferences copy so it can be recalled via the
1059
+ // low-threshold preference retrieval path (hardMinScore 0.25 vs 0.35).
1060
+ // Async & non-blocking: the primary write is already done.
1061
+ if (
1062
+ disposition !== "deduped" &&
1063
+ input.category !== "preferences" &&
1064
+ inferImplicitUsageSlot(input.text)
1065
+ ) {
1066
+ const prefCanonicalKey = buildDefaultCanonicalKey("preferences", input.text);
1067
+ const prefSlot = inferImplicitUsageSlot(input.text);
1068
+ const prefMetadata = buildStructuredMetadata({
1069
+ source: input.source,
1070
+ tags: [...(input.tags ?? []), "derived-preference", `derived-from:${entry.id}`],
1071
+ capture: "implicit_preference_dual_write_v1",
1072
+ category: "preferences",
1073
+ canonicalKey: prefCanonicalKey,
1074
+ extra: prefSlot ? { preferenceSlot: prefSlot } : undefined,
1075
+ narrativeInput: { scope: input.scope, text: input.text },
1076
+ });
1077
+ writeDurableEntry(deps, {
1078
+ text: input.text,
1079
+ vector,
1080
+ category: "preferences",
1081
+ scope: resolvedScope,
1082
+ importance: Math.min(input.importance, 0.5),
1083
+ metadata: prefMetadata,
1084
+ canonicalKey: prefCanonicalKey,
1085
+ source: input.source,
1086
+ language,
1087
+ fts_text,
1088
+ }).catch(() => {}); // Dual-write must never block the primary write
1089
+ }
1090
+
759
1091
  return toStoredRecord(input, entry, disposition, canonicalKey, conflictId);
760
1092
  }
761
1093
 
@@ -768,6 +1100,8 @@ export async function persistWorkflowPattern(
768
1100
  const tags = mergeTags(input.tags, ["workflow", "pattern"]);
769
1101
  const text = buildWorkflowPatternText(input);
770
1102
  const vector = await deps.embedder.embedPassage(text);
1103
+ const language = detectLang(text);
1104
+ const fts_text = tokenizeFts(text, language);
771
1105
  const canonicalKey = resolveCanonicalKey({
772
1106
  category: "patterns",
773
1107
  title: input.title,
@@ -783,6 +1117,8 @@ export async function persistWorkflowPattern(
783
1117
  metadata: buildWorkflowPatternMetadata(input, tags, canonicalKey),
784
1118
  canonicalKey,
785
1119
  source: input.source,
1120
+ language,
1121
+ fts_text,
786
1122
  });
787
1123
 
788
1124
  return {
@@ -808,6 +1144,8 @@ export async function persistCaseMemory(
808
1144
  const tags = mergeTags(input.tags, ["case", "solution"]);
809
1145
  const text = buildCaseMemoryText(input);
810
1146
  const vector = await deps.embedder.embedPassage(text);
1147
+ const language = detectLang(text);
1148
+ const fts_text = tokenizeFts(text, language);
811
1149
  const canonicalKey = resolveCanonicalKey({
812
1150
  category: "cases",
813
1151
  title: input.title,
@@ -823,6 +1161,8 @@ export async function persistCaseMemory(
823
1161
  metadata: buildCaseMemoryMetadata(input, tags, canonicalKey),
824
1162
  canonicalKey,
825
1163
  source: input.source,
1164
+ language,
1165
+ fts_text,
826
1166
  });
827
1167
 
828
1168
  return {
@@ -869,6 +1209,8 @@ export async function promoteMemory(
869
1209
  canonicalKey: input.canonicalKey,
870
1210
  });
871
1211
  const vector = await deps.embedder.embedPassage(text);
1212
+ const language = detectLang(text);
1213
+ const fts_text = tokenizeFts(text, language);
872
1214
  const resolvedScope = resolveScope(input);
873
1215
  const { entry, disposition, conflictId } = await writeDurableEntry(deps, {
874
1216
  text,
@@ -882,6 +1224,8 @@ export async function promoteMemory(
882
1224
  source: input.source,
883
1225
  sourceCategory: sourceEntry.category,
884
1226
  sourceBoundary: boundary,
1227
+ language,
1228
+ fts_text,
885
1229
  });
886
1230
 
887
1231
  return {
@@ -919,6 +1263,11 @@ export async function persistMemoryBatch(
919
1263
  }),
920
1264
  );
921
1265
 
1266
+ // Note: batch-internal cosine dedup (upstream #319) already runs in the ingest
1267
+ // pipeline (ingest.ts:batchInternalDedup) for large offline batches. For MCP
1268
+ // persistMemoryBatch (typically <10 items), per-item conflict detection (A-2)
1269
+ // in persistMemory handles dedup adequately without double-embedding cost.
1270
+
922
1271
  const persisted: StoredMemoryRecord[] = [];
923
1272
  for (const memory of normalizedItems) {
924
1273
  persisted.push(await persistMemory(deps, memory));
@@ -19,6 +19,7 @@
19
19
  */
20
20
 
21
21
  import type { MemoryStore, MemoryEntry } from "./store.js";
22
+ import { isActiveMemory } from "./memory-evolution.js";
22
23
 
23
24
  // ---------------------------------------------------------------------------
24
25
  // Config
@@ -85,10 +86,10 @@ export async function cascadeForget(
85
86
  // Skip the forgotten entry itself (may still be in index)
86
87
  if (entry.id === forgottenEntry.id) continue;
87
88
 
88
- // Skip already-archived entries
89
+ // Skip already-archived/superseded entries (unified via evolution.status)
90
+ if (!isActiveMemory(entry.metadata)) continue;
89
91
  let meta: Record<string, any> = {};
90
92
  try { meta = JSON.parse(entry.metadata || "{}"); } catch { /* skip */ }
91
- if (meta.state === "archived" || meta.state === "superseded") continue;
92
93
 
93
94
  // Proportional demotion: higher similarity → bigger cut
94
95
  // At sim=1.0 → full maxDemotion, at threshold → near-zero