recallnest 2.3.0 → 2.6.0

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 (258) hide show
  1. package/CHANGELOG.md +187 -0
  2. package/README.md +49 -38
  3. package/README_CN.md +46 -31
  4. package/assets/ui/app.js +1031 -0
  5. package/assets/ui/index.html +224 -0
  6. package/assets/ui/styles.css +781 -0
  7. package/config.json.example +4 -1
  8. package/docs/api-reference.md +1291 -0
  9. package/docs/architecture.md +168 -0
  10. package/docs/connector-spec.md +126 -0
  11. package/eval/cases-canary.json +58 -0
  12. package/eval/cases-scope-robustness.json +278 -0
  13. package/eval/cases.json +117 -20
  14. package/package.json +25 -4
  15. package/src/__tests__/access-tracker-novelty.test.ts +304 -0
  16. package/src/__tests__/activity-counter.test.ts +125 -0
  17. package/src/__tests__/adaptive-fidelity.test.ts +102 -0
  18. package/src/__tests__/admission-control.test.ts +206 -0
  19. package/src/__tests__/anchor-generator.test.ts +67 -0
  20. package/src/__tests__/api-server-security.test.ts +102 -0
  21. package/src/__tests__/auto-consolidation.test.ts +168 -0
  22. package/src/__tests__/auto-recall.test.ts +131 -0
  23. package/src/__tests__/babel-integration.test.ts +144 -0
  24. package/src/__tests__/batch-dedup.test.ts +65 -0
  25. package/src/__tests__/belief-history.test.ts +220 -0
  26. package/src/__tests__/canary-eval.test.ts +114 -0
  27. package/src/__tests__/capture-engine.test.ts +1328 -0
  28. package/src/__tests__/capture-heuristic.test.ts +171 -0
  29. package/src/__tests__/cascade-forget.test.ts +210 -0
  30. package/src/__tests__/category-min-scores.test.ts +116 -0
  31. package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
  32. package/src/__tests__/circuit-breaker.test.ts +99 -0
  33. package/src/__tests__/cluster-consolidation.test.ts +945 -0
  34. package/src/__tests__/confidence-metadata.test.ts +125 -0
  35. package/src/__tests__/confidence-tracker.test.ts +261 -0
  36. package/src/__tests__/conflict-advisor.test.ts +206 -0
  37. package/src/__tests__/conflict-escalation.test.ts +149 -0
  38. package/src/__tests__/conflict-lifecycle.test.ts +83 -0
  39. package/src/__tests__/conflict-output.test.ts +85 -0
  40. package/src/__tests__/conflict-store.test.ts +418 -0
  41. package/src/__tests__/connector-v1.test.ts +181 -0
  42. package/src/__tests__/consolidation-engine.test.ts +430 -0
  43. package/src/__tests__/constructive-retrieval.test.ts +509 -0
  44. package/src/__tests__/context-collapse-renderer.test.ts +214 -0
  45. package/src/__tests__/context-composer-pins.test.ts +64 -0
  46. package/src/__tests__/context-composer-task-selection.test.ts +24 -0
  47. package/src/__tests__/context-reconstructor.test.ts +65 -0
  48. package/src/__tests__/context-renderer.test.ts +91 -0
  49. package/src/__tests__/continuity-eval.test.ts +275 -0
  50. package/src/__tests__/continuity-schema.test.ts +97 -0
  51. package/src/__tests__/contracts.test.ts +262 -0
  52. package/src/__tests__/conversation-importer.test.ts +692 -0
  53. package/src/__tests__/core-recall-eval.test.ts +235 -0
  54. package/src/__tests__/core-summary.test.ts +109 -0
  55. package/src/__tests__/data-checkup.test.ts +276 -0
  56. package/src/__tests__/decay-exemption.test.ts +129 -0
  57. package/src/__tests__/dedup-actions.test.ts +58 -0
  58. package/src/__tests__/dedup-guard.test.ts +39 -0
  59. package/src/__tests__/deterministic-id.test.ts +39 -0
  60. package/src/__tests__/distill-lock.test.ts +305 -0
  61. package/src/__tests__/distill-session-argshape.test.ts +79 -0
  62. package/src/__tests__/doctor.test.ts +319 -0
  63. package/src/__tests__/dream-pipeline.test.ts +780 -0
  64. package/src/__tests__/drill-down.test.ts +108 -0
  65. package/src/__tests__/embedder.test.ts +122 -0
  66. package/src/__tests__/emotion-decay.test.ts +46 -0
  67. package/src/__tests__/emotion-detector.test.ts +56 -0
  68. package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
  69. package/src/__tests__/emotion-retrieval.test.ts +56 -0
  70. package/src/__tests__/entity-resolver.test.ts +58 -0
  71. package/src/__tests__/env-config.test.ts +151 -0
  72. package/src/__tests__/error-signature-boost.test.ts +87 -0
  73. package/src/__tests__/error-signature.test.ts +95 -0
  74. package/src/__tests__/error-taxonomy.test.ts +129 -0
  75. package/src/__tests__/ethics-layer.test.ts +476 -0
  76. package/src/__tests__/event-segmenter.test.ts +153 -0
  77. package/src/__tests__/feature-flags-integration.test.ts +54 -0
  78. package/src/__tests__/frequency-tracker.test.ts +113 -0
  79. package/src/__tests__/freshness.test.ts +186 -0
  80. package/src/__tests__/graph-export.test.ts +489 -0
  81. package/src/__tests__/hotness-blend.test.ts +283 -0
  82. package/src/__tests__/ingest-chunking.test.ts +44 -0
  83. package/src/__tests__/ingest.test.ts +360 -0
  84. package/src/__tests__/interference-detector.test.ts +203 -0
  85. package/src/__tests__/kg-extractor.test.ts +192 -0
  86. package/src/__tests__/kg-store.test.ts +300 -0
  87. package/src/__tests__/llm-client-pivot.test.ts +628 -0
  88. package/src/__tests__/llm-consolidation.test.ts +226 -0
  89. package/src/__tests__/maintenance-locks.test.ts +189 -0
  90. package/src/__tests__/mcp-efficiency.test.ts +288 -0
  91. package/src/__tests__/mcp-related-scopes.test.ts +151 -0
  92. package/src/__tests__/memory-boundaries.test.ts +282 -0
  93. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +345 -0
  94. package/src/__tests__/memory-evolution.test.ts +271 -0
  95. package/src/__tests__/memory-health-rebalance.test.ts +144 -0
  96. package/src/__tests__/memory-lint.test.ts +421 -0
  97. package/src/__tests__/memory-output.test.ts +215 -0
  98. package/src/__tests__/memory-promotion.test.ts +315 -0
  99. package/src/__tests__/migrate-dedup-ids.test.ts +65 -0
  100. package/src/__tests__/multi-vector.test.ts +242 -0
  101. package/src/__tests__/narrative-architecture.test.ts +456 -0
  102. package/src/__tests__/noise-filter.test.ts +205 -0
  103. package/src/__tests__/noise-prototype-bank.test.ts +280 -0
  104. package/src/__tests__/obsidian-connector.test.ts +363 -0
  105. package/src/__tests__/omitted-reporting.test.ts +98 -0
  106. package/src/__tests__/pending-review.test.ts +77 -0
  107. package/src/__tests__/pii-redact.test.ts +333 -0
  108. package/src/__tests__/pivot-apply.test.ts +373 -0
  109. package/src/__tests__/pivot-distill-supervisor.test.ts +457 -0
  110. package/src/__tests__/pivot-distill.test.ts +1178 -0
  111. package/src/__tests__/post-retrieval-filter.test.ts +178 -0
  112. package/src/__tests__/ppr-traversal.test.ts +256 -0
  113. package/src/__tests__/prediction-engine.test.ts +349 -0
  114. package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
  115. package/src/__tests__/preference-guard-smoke.test.ts +211 -0
  116. package/src/__tests__/preference-matcher.test.ts +282 -0
  117. package/src/__tests__/preference-slots.test.ts +205 -0
  118. package/src/__tests__/prospective-memory.test.ts +283 -0
  119. package/src/__tests__/pull-from-macbook-sync-only.test.ts +679 -0
  120. package/src/__tests__/query-entity-detector.test.ts +171 -0
  121. package/src/__tests__/query-expander-alias.test.ts +146 -0
  122. package/src/__tests__/query-expander.test.ts +34 -0
  123. package/src/__tests__/recall-gate.test.ts +266 -0
  124. package/src/__tests__/recall-governor.test.ts +227 -0
  125. package/src/__tests__/recall-mode.test.ts +64 -0
  126. package/src/__tests__/resolve-db-path.test.ts +130 -0
  127. package/src/__tests__/resolve-tier.test.ts +64 -0
  128. package/src/__tests__/result-synthesizer.test.ts +198 -0
  129. package/src/__tests__/resume-context.test.ts +4898 -0
  130. package/src/__tests__/retrieval-eval.test.ts +163 -0
  131. package/src/__tests__/retrieval-trace.test.ts +213 -0
  132. package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
  133. package/src/__tests__/retriever-lazy-init-dispatch.test.ts +123 -0
  134. package/src/__tests__/retroactive-boost.test.ts +223 -0
  135. package/src/__tests__/salience-scoring.test.ts +106 -0
  136. package/src/__tests__/scope-inventory.test.ts +200 -0
  137. package/src/__tests__/scope-policy.test.ts +69 -0
  138. package/src/__tests__/scope-suggester.test.ts +80 -0
  139. package/src/__tests__/session-checkpoint.test.ts +291 -0
  140. package/src/__tests__/session-distiller.test.ts +640 -0
  141. package/src/__tests__/session-gc.test.ts +215 -0
  142. package/src/__tests__/skill-engine.test.ts +552 -0
  143. package/src/__tests__/skill-promotion.test.ts +654 -0
  144. package/src/__tests__/skill-verifier.test.ts +122 -0
  145. package/src/__tests__/source-heartbeat.test.ts +250 -0
  146. package/src/__tests__/stable-context-length-contract.test.ts +62 -0
  147. package/src/__tests__/store-patch-metadata.test.ts +205 -0
  148. package/src/__tests__/store-read-consistency.test.ts +122 -0
  149. package/src/__tests__/store-schema-migration.test.ts +87 -0
  150. package/src/__tests__/store-scope-audit.test.ts +81 -0
  151. package/src/__tests__/store-upsert.test.ts +201 -0
  152. package/src/__tests__/supersede-chain.test.ts +154 -0
  153. package/src/__tests__/temporal-parser.test.ts +241 -0
  154. package/src/__tests__/temporal-validity.test.ts +160 -0
  155. package/src/__tests__/term-registry-terminal-roster.test.ts +127 -0
  156. package/src/__tests__/term-registry.test.ts +121 -0
  157. package/src/__tests__/tool-error-surfacing.test.ts +98 -0
  158. package/src/__tests__/tool-output-compressor.test.ts +160 -0
  159. package/src/__tests__/topic-tag.test.ts +113 -0
  160. package/src/__tests__/trusted-memory.test.ts +839 -0
  161. package/src/__tests__/ui-server-security.test.ts +93 -0
  162. package/src/__tests__/usage-tracker.test.ts +287 -0
  163. package/src/__tests__/version-manager.test.ts +213 -0
  164. package/src/__tests__/workflow-observation-skill-outcome.test.ts +352 -0
  165. package/src/__tests__/workflow-observation.test.ts +433 -0
  166. package/src/__tests__/write-verifier.test.ts +122 -0
  167. package/src/access-tracker.ts +90 -34
  168. package/src/activity-counter.ts +46 -18
  169. package/src/adaptive-retrieval.ts +11 -0
  170. package/src/aliases.ts +133 -0
  171. package/src/api-server.ts +36 -9
  172. package/src/audit-log.ts +4 -2
  173. package/src/auto-consolidation.ts +8 -3
  174. package/src/auto-gc.ts +85 -21
  175. package/src/auto-recall.ts +61 -1
  176. package/src/belief-history.ts +164 -0
  177. package/src/capture-engine.ts +142 -32
  178. package/src/cli.ts +268 -41
  179. package/src/confidence-tracker.ts +36 -18
  180. package/src/conflict-engine.ts +24 -3
  181. package/src/consolidation-engine.ts +255 -23
  182. package/src/context-composer-stable-selection.ts +7 -1
  183. package/src/context-composer.ts +8 -1
  184. package/src/data-checkup.ts +96 -7
  185. package/src/decay-engine.ts +59 -7
  186. package/src/distill-lock.ts +194 -15
  187. package/src/doctor.ts +13 -4
  188. package/src/dream-pipeline.ts +473 -41
  189. package/src/env-config.ts +118 -0
  190. package/src/error-signature.ts +153 -0
  191. package/src/error-taxonomy.ts +115 -0
  192. package/src/eval.ts +402 -14
  193. package/src/freshness.ts +192 -0
  194. package/src/http-request-policy.ts +148 -0
  195. package/src/idempotency.ts +8 -0
  196. package/src/ingest.ts +308 -12
  197. package/src/kg-extractor.ts +13 -3
  198. package/src/kg-store.ts +236 -44
  199. package/src/llm-client.ts +106 -11
  200. package/src/llm-consolidation.ts +2 -1
  201. package/src/mcp-server.ts +87 -1800
  202. package/src/mcp-tool-deps.ts +48 -0
  203. package/src/mcp-tools-advanced.ts +954 -0
  204. package/src/mcp-tools-core.ts +637 -0
  205. package/src/mcp-tools-governance.ts +475 -0
  206. package/src/memory-boundaries.ts +26 -3
  207. package/src/memory-evolution.ts +73 -1
  208. package/src/memory-health-rebalance.ts +4 -0
  209. package/src/memory-lint.ts +183 -10
  210. package/src/memory-output.ts +129 -4
  211. package/src/memory-promotion.ts +246 -0
  212. package/src/memory-schema.ts +25 -2
  213. package/src/memory-store-port.ts +46 -0
  214. package/src/multi-vector.ts +2 -1
  215. package/src/narrative-schema.ts +2 -1
  216. package/src/noise-filter.ts +57 -6
  217. package/src/noise-prototype-bank.ts +408 -0
  218. package/src/pii-detector.ts +207 -5
  219. package/src/pivot-apply.ts +399 -0
  220. package/src/preference-matcher.ts +37 -3
  221. package/src/query-expander.ts +90 -9
  222. package/src/recall-gate.ts +305 -0
  223. package/src/result-synthesizer.ts +2 -1
  224. package/src/retention-policy.ts +2 -1
  225. package/src/retrieval-profiles.ts +46 -4
  226. package/src/retriever.ts +354 -157
  227. package/src/retroactive-boost.ts +6 -2
  228. package/src/runtime-config.ts +78 -6
  229. package/src/scope-suggester.ts +113 -0
  230. package/src/session-distiller.ts +23 -2
  231. package/src/session-engine.ts +2 -1
  232. package/src/session-schema.ts +1 -0
  233. package/src/session-store.ts +5 -0
  234. package/src/skill-engine.ts +78 -0
  235. package/src/skill-promotion.ts +304 -48
  236. package/src/skill-schema.ts +16 -3
  237. package/src/skill-verifier.ts +152 -0
  238. package/src/store.ts +321 -57
  239. package/src/term-registry.ts +35 -0
  240. package/src/tool-output-compressor.ts +3 -1
  241. package/src/ui-server.ts +9 -1
  242. package/src/usage-tracker.ts +242 -0
  243. package/src/version-manager.ts +3 -2
  244. package/src/version.ts +19 -0
  245. package/src/workflow-observation-engine.ts +2 -1
  246. package/src/workflow-observation-schema.ts +2 -1
  247. package/src/workflow-observation-store.ts +6 -2
  248. package/.env +0 -4
  249. package/CLAUDE.md +0 -46
  250. package/config.json +0 -55
  251. package/daily-recall.sh +0 -110
  252. package/logs/daily-recall.error.log +0 -0
  253. package/logs/daily-recall.log +0 -60
  254. package/logs/ingest-2026-04-11.log +0 -110
  255. package/logs/ingest-2026-04-12.log +0 -90
  256. package/logs/ingest-2026-04-13.log +0 -87
  257. package/logs/launchagent-stderr.log +0 -0
  258. package/logs/launchagent-stdout.log +0 -0
@@ -1,13 +1,15 @@
1
1
  import type { Embedder } from "./embedder.js";
2
2
  import { detectLang, tokenizeFts } from "./language-hook.js";
3
3
  import { generateAnchor } from "./anchor-generator.js";
4
- import { incrementWriteCount } from "./activity-counter.js";
4
+ import { extractErrorSignatures } from "./error-signature.js";
5
5
  import { verifyWrite } from "./write-verifier.js";
6
6
  // batchInternalDedup is available in ingest.ts for large-batch scenarios;
7
7
  // persistMemoryBatch relies on per-item conflict detection (A-2) instead.
8
- import { defaultEvolution, buildSupersedeMetadata, buildSupersedeMetadataForNew, buildPendingReviewMetadata, isActiveMemory, patchEvolution } from "./memory-evolution.js";
8
+ import { defaultEvolution, buildSupersedeMetadata, buildSupersedeMetadataForNew, buildPendingReviewMetadata, isActiveMemory, parseEvolution, patchEvolution } from "./memory-evolution.js";
9
+ import { archiveBeliefVersion, buildSupersedingBeliefMetadata } from "./belief-history.js";
9
10
  import { detectTopicTag, injectTopicTag } from "./topic-tag.js";
10
11
  import { assignDefaultConfidence, type ConfidenceMetadata } from "./confidence-tracker.js";
12
+ import { parseDependsOnInput } from "./freshness.js";
11
13
  import {
12
14
  type CaseMemoryInput,
13
15
  CaseMemoryInputSchema,
@@ -56,12 +58,13 @@ import {
56
58
  import { matchPreference, applyPreferenceMatch } from "./preference-matcher.js";
57
59
  import type { LLMClient } from "./llm-client.js";
58
60
  import type { KGExtractor } from "./kg-extractor.js";
59
- import { scanForPII } from "./pii-detector.js";
61
+ import { scanForPII, redactSecrets } from "./pii-detector.js";
60
62
  import type { AuditLogger } from "./audit-log.js";
61
63
  import { checkAdmission, type ScopeRateLimiter, type AdmissionConfig } from "./admission-control.js";
64
+ import { resolveNoisePrototypeBank, type NoisePrototypeBank } from "./noise-prototype-bank.js";
62
65
  import { tagNarrativeIfEnabled } from "./narrative-tagger.js";
63
66
 
64
- type StoreDeps = Pick<MemoryStore, "store"> & Partial<Pick<MemoryStore, "list" | "update" | "getById" | "get" | "vectorSearch">>;
67
+ type StoreDeps = Pick<MemoryStore, "store"> & Partial<Pick<MemoryStore, "list" | "update" | "getById" | "get" | "vectorSearch" | "upsert">>;
65
68
  type ConflictStoreDeps = Pick<ConflictCandidateStore, "save" | "replace" | "getOpenByFingerprint" | "getLatestByFingerprint">;
66
69
 
67
70
  export interface PersistMemoryDeps {
@@ -78,9 +81,15 @@ export interface PersistMemoryDeps {
78
81
  rateLimiter?: ScopeRateLimiter | null;
79
82
  /** LME-8: Optional admission config overrides */
80
83
  admissionConfig?: Partial<AdmissionConfig>;
84
+ /**
85
+ * Noise-prototype shadow experiment (mlp borrow). undefined → env-governed
86
+ * shared instance (RECALLNEST_NOISE_PROTOTYPE, shadow default); null → off.
87
+ * Shadow-only: it observes and logs, it never rejects.
88
+ */
89
+ noisePrototypeBank?: NoisePrototypeBank | null;
81
90
  }
82
91
 
83
- interface DurableWriteInput {
92
+ export interface DurableWriteInput {
84
93
  text: string;
85
94
  vector: number[];
86
95
  category: DurableMemoryCategory;
@@ -140,7 +149,7 @@ function mergeTags(...groups: string[][]): string[] {
140
149
  return merged.slice(0, 8);
141
150
  }
142
151
 
143
- function buildStructuredMetadata(params: {
152
+ export function buildStructuredMetadata(params: {
144
153
  source: string;
145
154
  tags: string[];
146
155
  capture: string;
@@ -437,11 +446,15 @@ async function findCanonicalMatches(
437
446
  return entries.filter((entry) => {
438
447
  const boundary = extractBoundaryMetadata(entry.metadata);
439
448
  if (boundary?.layer !== "durable") return false;
449
+ // Belief-history rows carry the same canonicalKey as the live row they were copied
450
+ // from. Without this guard they'd be treated as dedupe/conflict targets — so rewriting
451
+ // a belief back to an older wording would look like a duplicate, not a change.
452
+ if (!isActiveMemory(entry.metadata)) return false;
440
453
  return extractCanonicalKey(entry.metadata) === canonicalKey;
441
454
  });
442
455
  }
443
456
 
444
- async function writeDurableEntry(
457
+ export async function writeDurableEntry(
445
458
  deps: PersistMemoryDeps,
446
459
  params: DurableWriteInput,
447
460
  ): Promise<{
@@ -563,13 +576,26 @@ async function writeDurableEntry(
563
576
  }
564
577
  }
565
578
 
579
+ // latest-wins is where beliefs change. Archive the version being replaced first —
580
+ // overwriting in place used to delete the old text + vector outright, which is what
581
+ // made "what did I believe last month?" unanswerable no matter how retrieval queried.
582
+ const now = Date.now();
583
+ const previousEvo = parseEvolution(latest.metadata, latest.timestamp);
584
+ const { historyId } = await archiveBeliefVersion(
585
+ { store: deps.store, embedder: deps.embedder },
586
+ latest,
587
+ { now },
588
+ );
589
+
566
590
  const updated = await deps.store.update(latest.id, {
567
591
  text: params.text,
568
592
  vector: params.vector,
569
593
  importance: params.importance,
570
594
  category: params.category,
571
- metadata: params.metadata,
572
- timestamp: Date.now(),
595
+ // Merge rather than replace: a bare params.metadata would wipe the evolution block
596
+ // (version, access stats, supersede links) along with the text.
597
+ metadata: buildSupersedingBeliefMetadata(params.metadata, previousEvo, historyId, now),
598
+ timestamp: now,
573
599
  ...(params.language ? { language: params.language } : {}),
574
600
  ...(params.fts_text ? { fts_text: params.fts_text } : {}),
575
601
  });
@@ -582,16 +608,38 @@ async function writeDurableEntry(
582
608
  };
583
609
  }
584
610
 
611
+ // No canonical match was found, so this falls through to store(). With a canonicalKey the
612
+ // id is deterministic and store() upserts on id — so a row already sitting at that id is
613
+ // overwritten here without ever reaching the latest-wins branch above. findCanonicalMatches
614
+ // only scans the most recent CANONICAL_SCAN_LIMIT rows, so any canonical row older than that
615
+ // window lands on this path. Look it up by id directly, which no scan limit can hide.
616
+ const targetId = params.canonicalKey
617
+ ? deterministicId(params.scope, params.canonicalKey)
618
+ : undefined;
619
+ let metadataForStore = params.metadata;
620
+
621
+ if (targetId && deps.store.getById) {
622
+ const rowAtTargetId = await deps.store.getById(targetId);
623
+ if (rowAtTargetId && normalizeMemoryText(rowAtTargetId.text) !== normalizeMemoryText(params.text)) {
624
+ const now = Date.now();
625
+ const previousEvo = parseEvolution(rowAtTargetId.metadata, rowAtTargetId.timestamp);
626
+ const { historyId } = await archiveBeliefVersion(
627
+ { store: deps.store, embedder: deps.embedder },
628
+ rowAtTargetId,
629
+ { now },
630
+ );
631
+ metadataForStore = buildSupersedingBeliefMetadata(params.metadata, previousEvo, historyId, now);
632
+ }
633
+ }
634
+
585
635
  const stored = await deps.store.store({
586
- id: params.canonicalKey
587
- ? deterministicId(params.scope, params.canonicalKey)
588
- : undefined,
636
+ id: targetId,
589
637
  text: params.text,
590
638
  vector: params.vector,
591
639
  category: params.category,
592
640
  scope: params.scope,
593
641
  importance: params.importance,
594
- metadata: params.metadata,
642
+ metadata: metadataForStore,
595
643
  ...(params.language ? { language: params.language } : {}),
596
644
  ...(params.fts_text ? { fts_text: params.fts_text } : {}),
597
645
  });
@@ -699,6 +747,17 @@ function buildCaseMemoryMetadata(
699
747
  tools: input.tools,
700
748
  },
701
749
  };
750
+ // A1: error-signature 指纹(读写对称,P3-A 检索复用同一抽取器)。
751
+ // 在 build metadata 前算好作为 extra 写入,不 post-write update(Codex 验证要求)。
752
+ const errorSignature = extractErrorSignatures({
753
+ problem: input.problem,
754
+ context: input.context,
755
+ outcome: input.outcome,
756
+ solutionSteps: input.solutionSteps,
757
+ });
758
+ if (errorSignature.length > 0) cmExtra.error_signature = errorSignature;
759
+ // A1: break-loop 五维(可选,向后兼容;不提供则普通 case)。
760
+ if (input.debugFraming) cmExtra.debugFraming = input.debugFraming;
702
761
  const anchor = generateAnchor(input.title + ": " + input.problem, cmExtra);
703
762
  if (anchor) cmExtra.anchor = anchor;
704
763
  return buildStructuredMetadata({
@@ -746,7 +805,16 @@ export async function persistMemory(
746
805
  deps: PersistMemoryDeps,
747
806
  rawInput: unknown,
748
807
  ): Promise<StoredMemoryRecord> {
749
- const input = StoreMemoryInputSchema.parse(rawInput);
808
+ let input = StoreMemoryInputSchema.parse(rawInput);
809
+
810
+ // F-3b: Scan THEN redact, before the text reaches any external surface —
811
+ // LLM summary/importance calls, embedding API, storage, audit log, KG.
812
+ // Scan first so the piiWarning below still reflects what the raw text held.
813
+ const piiScan = scanForPII(input.text);
814
+ const redaction = redactSecrets(input.text);
815
+ if (redaction.redacted > 0) {
816
+ input = { ...input, text: redaction.text };
817
+ }
750
818
 
751
819
  // F3: Extract temporal validity params before Zod strips them
752
820
  const rawObj = rawInput as Record<string, unknown> | null;
@@ -801,6 +869,28 @@ export async function persistMemory(
801
869
  deps.admissionConfig,
802
870
  );
803
871
  if (admission.verdict === "rejected") {
872
+ // Gemini #26523 lesson: rejected writes must stay queryable after the fact,
873
+ // not just echo back to the caller — otherwise they become invisible noise.
874
+ try {
875
+ deps.auditLogger?.log({
876
+ operation: "reject",
877
+ scope: admissionScope,
878
+ actor: input.source,
879
+ details: `${admission.reason ?? "unknown"}: ${input.text.slice(0, 100)}`,
880
+ });
881
+ } catch {
882
+ // Audit must never block memory writes
883
+ }
884
+ // Noise-prototype shadow experiment: learn ONLY from the regex noise
885
+ // filter's own verdict (mlp #914 lesson — a heuristic's low-importance
886
+ // self-judgment must not be recycled as a noise label). Embedding only
887
+ // happens on a repeat sighting under the bank cap, so this stays cheap.
888
+ if (admission.reason === "noise_detected") {
889
+ const bank = resolveNoisePrototypeBank(deps.noisePrototypeBank);
890
+ if (bank) {
891
+ await bank.learnFromRejection(input.text, (text) => deps.embedder.embedPassage(text));
892
+ }
893
+ }
804
894
  return {
805
895
  id: `rejected-${Date.now()}`,
806
896
  text: input.text,
@@ -812,10 +902,20 @@ export async function persistMemory(
812
902
  resolvedScope: admissionScope,
813
903
  storedAt: new Date().toISOString(),
814
904
  disposition: "rejected" as const,
905
+ ...(admission.reason ? { rejectionReason: admission.reason } : {}),
815
906
  };
816
907
  }
817
908
 
818
909
  const vector = await deps.embedder.embedPassage(input.text);
910
+
911
+ // Noise-prototype shadow experiment, matching side: reuses the write-path
912
+ // embedding just computed (zero extra embedding). Log-only — the verdict
913
+ // never blocks this write while the experiment is in shadow.
914
+ {
915
+ const bank = resolveNoisePrototypeBank(deps.noisePrototypeBank);
916
+ bank?.matchShadow(input.text, vector);
917
+ }
918
+
819
919
  const language = detectLang(input.text);
820
920
  const fts_text = tokenizeFts(input.text, language);
821
921
  const resolvedScope = resolveScope(input);
@@ -843,6 +943,7 @@ export async function persistMemory(
843
943
  matchResult,
844
944
  deps.store as MemoryStore,
845
945
  resolvedScope,
946
+ { embedder: deps.embedder },
846
947
  );
847
948
  if (applied.handled) {
848
949
  const entry = applied.entry ?? {
@@ -937,15 +1038,16 @@ export async function persistMemory(
937
1038
  }
938
1039
 
939
1040
  // F-3: PII detection — inject warning into metadata if PII found.
940
- // Non-blocking: PII does not prevent the write.
1041
+ // Non-blocking: PII does not prevent the write. Uses the pre-redaction scan
1042
+ // from the top of persistMemory (scanning here would see scrubbed text).
941
1043
  let metadata = buildStoreMemoryMetadata(input, canonicalKey);
942
- const piiResult = scanForPII(input.text);
943
- if (piiResult.hasPII) {
1044
+ if (piiScan.hasPII) {
944
1045
  const parsed: Record<string, unknown> = JSON.parse(metadata);
945
1046
  parsed.piiWarning = {
946
- summary: piiResult.summary,
947
- detections: piiResult.detections.length,
948
- severity: piiResult.detections.some(d => d.severity === "high") ? "high" : "medium",
1047
+ summary: piiScan.summary,
1048
+ detections: piiScan.detections.length,
1049
+ severity: piiScan.detections.some(d => d.severity === "high") ? "high" : "medium",
1050
+ ...(redaction.redacted > 0 ? { redacted: redaction.redacted } : {}),
949
1051
  };
950
1052
  metadata = JSON.stringify(parsed);
951
1053
  }
@@ -995,6 +1097,15 @@ export async function persistMemory(
995
1097
  metadata = JSON.stringify(parsed);
996
1098
  }
997
1099
 
1100
+ // Freshness (borrowed 2026-07-21): opt-in dependsOn declarations ride in metadata like
1101
+ // validUntil/confidence do — pulled from raw input so StoreMemoryInputSchema stays untouched.
1102
+ const dependsOn = parseDependsOnInput(rawObj?.dependsOn);
1103
+ if (dependsOn) {
1104
+ const parsed: Record<string, unknown> = JSON.parse(metadata);
1105
+ parsed.dependsOn = dependsOn;
1106
+ metadata = JSON.stringify(parsed);
1107
+ }
1108
+
998
1109
  const { entry, disposition, conflictId } = await writeDurableEntry(deps, {
999
1110
  text: input.text,
1000
1111
  vector,
@@ -1031,14 +1142,9 @@ export async function persistMemory(
1031
1142
  // Audit must never block memory writes
1032
1143
  }
1033
1144
 
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
- }
1145
+ // HP-3 activity counter moved to the store layer (store/upsert/storeBatch count per
1146
+ // scope): that is the necessary chokepoint for every write source (capture/ingest/
1147
+ // store_memory/consolidation), and per-scope counts drive per-scope dream activation.
1042
1148
 
1043
1149
  // HP-2: Post-write verification — async, non-blocking
1044
1150
  if (disposition !== "deduped" && deps.store.get) {
@@ -1063,7 +1169,7 @@ export async function persistMemory(
1063
1169
  input.category !== "preferences" &&
1064
1170
  inferImplicitUsageSlot(input.text)
1065
1171
  ) {
1066
- const prefCanonicalKey = buildDefaultCanonicalKey("preferences", input.text);
1172
+ const prefCanonicalKey = buildDefaultCanonicalKey({ category: "preferences", text: input.text });
1067
1173
  const prefSlot = inferImplicitUsageSlot(input.text);
1068
1174
  const prefMetadata = buildStructuredMetadata({
1069
1175
  source: input.source,
@@ -1098,7 +1204,8 @@ export async function persistWorkflowPattern(
1098
1204
  const input = WorkflowPatternInputSchema.parse(rawInput);
1099
1205
  const resolvedScope = resolveScope(input);
1100
1206
  const tags = mergeTags(input.tags, ["workflow", "pattern"]);
1101
- const text = buildWorkflowPatternText(input);
1207
+ // F-3b: scrub before embedding/storage (this path bypasses persistMemory)
1208
+ const text = redactSecrets(buildWorkflowPatternText(input)).text;
1102
1209
  const vector = await deps.embedder.embedPassage(text);
1103
1210
  const language = detectLang(text);
1104
1211
  const fts_text = tokenizeFts(text, language);
@@ -1142,7 +1249,8 @@ export async function persistCaseMemory(
1142
1249
  const input = CaseMemoryInputSchema.parse(rawInput);
1143
1250
  const resolvedScope = resolveScope(input);
1144
1251
  const tags = mergeTags(input.tags, ["case", "solution"]);
1145
- const text = buildCaseMemoryText(input);
1252
+ // F-3b: scrub before embedding/storage (this path bypasses persistMemory)
1253
+ const text = redactSecrets(buildCaseMemoryText(input)).text;
1146
1254
  const vector = await deps.embedder.embedPassage(text);
1147
1255
  const language = detectLang(text);
1148
1256
  const fts_text = tokenizeFts(text, language);
@@ -1202,7 +1310,9 @@ export async function promoteMemory(
1202
1310
  }
1203
1311
 
1204
1312
  const category = inferPromotedCategory(input, sourceEntry);
1205
- const text = input.text || sourceEntry.text;
1313
+ // F-3b: scrub caller-supplied text AND legacy stored text (pre-redaction
1314
+ // era entries may still hold secrets; promotion is a natural wash point)
1315
+ const text = redactSecrets(input.text || sourceEntry.text).text;
1206
1316
  const canonicalKey = resolveCanonicalKey({
1207
1317
  category,
1208
1318
  text,