recallnest 2.2.1 → 2.5.4

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 (227) hide show
  1. package/CHANGELOG.md +150 -0
  2. package/README.md +46 -35
  3. package/README_CN.md +41 -26
  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/connectors/examples/email-adapter.ts +73 -0
  9. package/connectors/examples/logs-adapter.ts +103 -0
  10. package/connectors/examples/rss-adapter.ts +95 -0
  11. package/docs/api-reference.md +1291 -0
  12. package/docs/architecture.md +168 -0
  13. package/docs/connector-spec.md +126 -0
  14. package/eval/cases-canary.json +58 -0
  15. package/eval/cases-scope-robustness.json +278 -0
  16. package/eval/cases.json +117 -20
  17. package/package.json +22 -1
  18. package/src/__tests__/access-tracker-novelty.test.ts +238 -0
  19. package/src/__tests__/activity-counter.test.ts +125 -0
  20. package/src/__tests__/adaptive-fidelity.test.ts +102 -0
  21. package/src/__tests__/admission-control.test.ts +206 -0
  22. package/src/__tests__/anchor-generator.test.ts +67 -0
  23. package/src/__tests__/auto-consolidation.test.ts +168 -0
  24. package/src/__tests__/auto-recall.test.ts +131 -0
  25. package/src/__tests__/babel-integration.test.ts +144 -0
  26. package/src/__tests__/batch-dedup.test.ts +65 -0
  27. package/src/__tests__/canary-eval.test.ts +114 -0
  28. package/src/__tests__/capture-engine.test.ts +1238 -0
  29. package/src/__tests__/capture-heuristic.test.ts +171 -0
  30. package/src/__tests__/cascade-forget.test.ts +210 -0
  31. package/src/__tests__/category-min-scores.test.ts +33 -0
  32. package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
  33. package/src/__tests__/circuit-breaker.test.ts +99 -0
  34. package/src/__tests__/cluster-consolidation.test.ts +782 -0
  35. package/src/__tests__/confidence-metadata.test.ts +125 -0
  36. package/src/__tests__/confidence-tracker.test.ts +261 -0
  37. package/src/__tests__/conflict-advisor.test.ts +206 -0
  38. package/src/__tests__/conflict-escalation.test.ts +149 -0
  39. package/src/__tests__/conflict-lifecycle.test.ts +83 -0
  40. package/src/__tests__/conflict-output.test.ts +85 -0
  41. package/src/__tests__/conflict-store.test.ts +374 -0
  42. package/src/__tests__/connector-v1.test.ts +181 -0
  43. package/src/__tests__/consolidation-engine.test.ts +333 -0
  44. package/src/__tests__/constructive-retrieval.test.ts +509 -0
  45. package/src/__tests__/context-collapse-renderer.test.ts +214 -0
  46. package/src/__tests__/context-composer-pins.test.ts +64 -0
  47. package/src/__tests__/context-composer-task-selection.test.ts +24 -0
  48. package/src/__tests__/context-reconstructor.test.ts +65 -0
  49. package/src/__tests__/context-renderer.test.ts +91 -0
  50. package/src/__tests__/continuity-eval.test.ts +275 -0
  51. package/src/__tests__/continuity-schema.test.ts +97 -0
  52. package/src/__tests__/contracts.test.ts +216 -0
  53. package/src/__tests__/conversation-importer.test.ts +692 -0
  54. package/src/__tests__/core-recall-eval.test.ts +235 -0
  55. package/src/__tests__/core-summary.test.ts +109 -0
  56. package/src/__tests__/data-checkup.test.ts +276 -0
  57. package/src/__tests__/decay-exemption.test.ts +84 -0
  58. package/src/__tests__/dedup-actions.test.ts +58 -0
  59. package/src/__tests__/dedup-guard.test.ts +39 -0
  60. package/src/__tests__/deterministic-id.test.ts +39 -0
  61. package/src/__tests__/distill-lock.test.ts +305 -0
  62. package/src/__tests__/distill-session-argshape.test.ts +79 -0
  63. package/src/__tests__/doctor.test.ts +319 -0
  64. package/src/__tests__/dream-pipeline.test.ts +321 -0
  65. package/src/__tests__/drill-down.test.ts +108 -0
  66. package/src/__tests__/embedder.test.ts +122 -0
  67. package/src/__tests__/emotion-decay.test.ts +46 -0
  68. package/src/__tests__/emotion-detector.test.ts +56 -0
  69. package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
  70. package/src/__tests__/emotion-retrieval.test.ts +56 -0
  71. package/src/__tests__/entity-resolver.test.ts +58 -0
  72. package/src/__tests__/env-config.test.ts +151 -0
  73. package/src/__tests__/error-signature-boost.test.ts +87 -0
  74. package/src/__tests__/error-signature.test.ts +95 -0
  75. package/src/__tests__/error-taxonomy.test.ts +129 -0
  76. package/src/__tests__/ethics-layer.test.ts +476 -0
  77. package/src/__tests__/event-segmenter.test.ts +153 -0
  78. package/src/__tests__/feature-flags-integration.test.ts +54 -0
  79. package/src/__tests__/frequency-tracker.test.ts +113 -0
  80. package/src/__tests__/graph-export.test.ts +489 -0
  81. package/src/__tests__/hotness-blend.test.ts +76 -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-consolidation.test.ts +226 -0
  88. package/src/__tests__/maintenance-locks.test.ts +174 -0
  89. package/src/__tests__/mcp-efficiency.test.ts +288 -0
  90. package/src/__tests__/mcp-related-scopes.test.ts +151 -0
  91. package/src/__tests__/memory-boundaries.test.ts +250 -0
  92. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +345 -0
  93. package/src/__tests__/memory-evolution.test.ts +271 -0
  94. package/src/__tests__/memory-health-rebalance.test.ts +144 -0
  95. package/src/__tests__/memory-lint.test.ts +421 -0
  96. package/src/__tests__/memory-output.test.ts +215 -0
  97. package/src/__tests__/memory-promotion.test.ts +315 -0
  98. package/src/__tests__/migrate-dedup-ids.test.ts +65 -0
  99. package/src/__tests__/multi-vector.test.ts +242 -0
  100. package/src/__tests__/narrative-architecture.test.ts +456 -0
  101. package/src/__tests__/noise-filter.test.ts +205 -0
  102. package/src/__tests__/obsidian-connector.test.ts +363 -0
  103. package/src/__tests__/omitted-reporting.test.ts +98 -0
  104. package/src/__tests__/pending-review.test.ts +77 -0
  105. package/src/__tests__/pii-redact.test.ts +314 -0
  106. package/src/__tests__/post-retrieval-filter.test.ts +178 -0
  107. package/src/__tests__/ppr-traversal.test.ts +256 -0
  108. package/src/__tests__/prediction-engine.test.ts +349 -0
  109. package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
  110. package/src/__tests__/preference-guard-smoke.test.ts +211 -0
  111. package/src/__tests__/preference-matcher.test.ts +230 -0
  112. package/src/__tests__/preference-slots.test.ts +205 -0
  113. package/src/__tests__/prospective-memory.test.ts +283 -0
  114. package/src/__tests__/query-entity-detector.test.ts +171 -0
  115. package/src/__tests__/query-expander-alias.test.ts +146 -0
  116. package/src/__tests__/query-expander.test.ts +34 -0
  117. package/src/__tests__/recall-governor.test.ts +227 -0
  118. package/src/__tests__/recall-mode.test.ts +64 -0
  119. package/src/__tests__/result-synthesizer.test.ts +198 -0
  120. package/src/__tests__/resume-context.test.ts +4898 -0
  121. package/src/__tests__/retrieval-trace.test.ts +213 -0
  122. package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
  123. package/src/__tests__/retroactive-boost.test.ts +223 -0
  124. package/src/__tests__/salience-scoring.test.ts +106 -0
  125. package/src/__tests__/scope-inventory.test.ts +200 -0
  126. package/src/__tests__/scope-policy.test.ts +69 -0
  127. package/src/__tests__/scope-suggester.test.ts +80 -0
  128. package/src/__tests__/session-checkpoint.test.ts +291 -0
  129. package/src/__tests__/session-distiller.test.ts +640 -0
  130. package/src/__tests__/session-gc.test.ts +215 -0
  131. package/src/__tests__/skill-engine.test.ts +552 -0
  132. package/src/__tests__/skill-promotion.test.ts +601 -0
  133. package/src/__tests__/skill-verifier.test.ts +122 -0
  134. package/src/__tests__/source-heartbeat.test.ts +250 -0
  135. package/src/__tests__/store-patch-metadata.test.ts +205 -0
  136. package/src/__tests__/store-scope-audit.test.ts +81 -0
  137. package/src/__tests__/store-upsert.test.ts +184 -0
  138. package/src/__tests__/supersede-chain.test.ts +154 -0
  139. package/src/__tests__/temporal-parser.test.ts +241 -0
  140. package/src/__tests__/temporal-validity.test.ts +160 -0
  141. package/src/__tests__/term-registry.test.ts +121 -0
  142. package/src/__tests__/tool-error-surfacing.test.ts +98 -0
  143. package/src/__tests__/tool-output-compressor.test.ts +160 -0
  144. package/src/__tests__/topic-tag.test.ts +113 -0
  145. package/src/__tests__/trusted-memory.test.ts +839 -0
  146. package/src/__tests__/usage-tracker.test.ts +287 -0
  147. package/src/__tests__/version-manager.test.ts +213 -0
  148. package/src/__tests__/workflow-observation-skill-outcome.test.ts +352 -0
  149. package/src/__tests__/workflow-observation.test.ts +433 -0
  150. package/src/__tests__/write-verifier.test.ts +122 -0
  151. package/src/access-tracker.ts +38 -32
  152. package/src/activity-counter.ts +46 -18
  153. package/src/aliases.ts +133 -0
  154. package/src/api-server.ts +27 -7
  155. package/src/audit-log.ts +4 -2
  156. package/src/auto-consolidation.ts +8 -3
  157. package/src/auto-gc.ts +85 -21
  158. package/src/capture-engine.ts +54 -21
  159. package/src/cli.ts +199 -37
  160. package/src/confidence-tracker.ts +36 -18
  161. package/src/connector-types.ts +57 -0
  162. package/src/consolidation-engine.ts +109 -11
  163. package/src/context-composer.ts +8 -1
  164. package/src/conversation-importer.ts +25 -1
  165. package/src/data-checkup.ts +126 -7
  166. package/src/distill-lock.ts +194 -15
  167. package/src/doctor.ts +38 -2
  168. package/src/dream-pipeline.ts +118 -34
  169. package/src/env-config.ts +77 -0
  170. package/src/error-signature.ts +153 -0
  171. package/src/error-taxonomy.ts +115 -0
  172. package/src/eval.ts +251 -6
  173. package/src/idempotency.ts +8 -0
  174. package/src/ingest.ts +234 -10
  175. package/src/kg-extractor.ts +6 -2
  176. package/src/kg-store.ts +236 -44
  177. package/src/llm-consolidation.ts +2 -1
  178. package/src/mcp-server.ts +85 -1799
  179. package/src/mcp-tool-deps.ts +48 -0
  180. package/src/mcp-tools-advanced.ts +941 -0
  181. package/src/mcp-tools-core.ts +633 -0
  182. package/src/mcp-tools-governance.ts +475 -0
  183. package/src/memory-boundaries.ts +3 -1
  184. package/src/memory-evolution.ts +73 -1
  185. package/src/memory-lint.ts +183 -10
  186. package/src/memory-output.ts +114 -3
  187. package/src/memory-promotion.ts +246 -0
  188. package/src/memory-schema.ts +25 -2
  189. package/src/memory-store-port.ts +43 -0
  190. package/src/multi-vector.ts +2 -1
  191. package/src/narrative-schema.ts +2 -1
  192. package/src/noise-filter.ts +57 -6
  193. package/src/obsidian-connector.ts +282 -0
  194. package/src/pii-detector.ts +157 -5
  195. package/src/query-expander.ts +90 -9
  196. package/src/result-synthesizer.ts +2 -1
  197. package/src/retention-policy.ts +2 -1
  198. package/src/retriever.ts +259 -139
  199. package/src/runtime-config.ts +8 -1
  200. package/src/scope-suggester.ts +113 -0
  201. package/src/session-distiller.ts +23 -2
  202. package/src/session-engine.ts +2 -1
  203. package/src/session-schema.ts +3 -2
  204. package/src/session-store.ts +5 -0
  205. package/src/skill-engine.ts +78 -0
  206. package/src/skill-promotion.ts +247 -47
  207. package/src/skill-schema.ts +16 -3
  208. package/src/skill-verifier.ts +152 -0
  209. package/src/source-heartbeat.ts +108 -0
  210. package/src/store.ts +267 -38
  211. package/src/tool-output-compressor.ts +3 -1
  212. package/src/ui-server.ts +2 -1
  213. package/src/usage-tracker.ts +242 -0
  214. package/src/version-manager.ts +3 -2
  215. package/src/workflow-observation-engine.ts +2 -1
  216. package/src/workflow-observation-schema.ts +2 -1
  217. package/src/workflow-observation-store.ts +6 -2
  218. package/.env +0 -4
  219. package/CLAUDE.md +0 -46
  220. package/config.json +0 -55
  221. package/daily-recall.sh +0 -110
  222. package/logs/daily-recall.error.log +0 -0
  223. package/logs/daily-recall.log +0 -30
  224. package/logs/ingest-2026-04-11.log +0 -110
  225. package/logs/ingest-2026-04-12.log +0 -90
  226. package/logs/launchagent-stderr.log +0 -0
  227. package/logs/launchagent-stdout.log +0 -0
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Promote-Scan: auto-promotion pipeline for transcript-downgraded stable facts.
3
+ *
4
+ * Transcript ingest downgrades profile/preferences into `events` evidence
5
+ * (memory-boundaries.ts:resolveIngestBoundary), tagging boundary.downgradedFrom.
6
+ * Without an automatic promotion path those high-signal facts stay sparse in
7
+ * durable memory. This module clusters recurring downgraded evidence and, when a
8
+ * cluster recurs often enough at high enough importance, promotes the seed back
9
+ * to its original durable category via the injected `promote` callback.
10
+ *
11
+ * Idempotency is delegated to promoteMemory -> writeDurableEntry's canonicalKey
12
+ * dedup, so re-scanning is safe (we do not mutate source evidence — see #19
13
+ * non-atomic store.update — and rely on dedup instead of a promotedTo marker).
14
+ */
15
+
16
+ import type { MemoryEntry, MemoryStore } from "./store.js";
17
+ import type { DurableMemoryCategory, StoredPromotedMemoryRecord } from "./memory-schema.js";
18
+ import { isActiveMemory } from "./memory-evolution.js";
19
+ import { isNoise } from "./noise-filter.js";
20
+ import { extractBoundaryMetadata, parseMetadataObject } from "./memory-boundaries.js";
21
+ import { greedyCluster } from "./skill-promotion.js";
22
+ import { promoteMemory, type PersistMemoryDeps } from "./capture-engine.js";
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Types
26
+ // ---------------------------------------------------------------------------
27
+
28
+ /** Categories that transcript ingest downgrades into evidence. */
29
+ type PromotableDowngrade = "profile" | "preferences";
30
+
31
+ export interface PromoteRequest {
32
+ memoryId: string;
33
+ category: DurableMemoryCategory;
34
+ scope: string;
35
+ importance: number;
36
+ }
37
+
38
+ export type PromoteFn = (req: PromoteRequest) => Promise<StoredPromotedMemoryRecord>;
39
+
40
+ export interface PromoteScanDeps {
41
+ store: Pick<MemoryStore, "list" | "getVectors">;
42
+ /** Injected so the scan unit-tests without the full persist pipeline. */
43
+ promote: PromoteFn;
44
+ }
45
+
46
+ export interface PromoteScanConfig {
47
+ /** Min cluster members before a downgraded fact is promoted (default 3). */
48
+ minOccurrences: number;
49
+ /** Min average cluster importance to promote (default 0.6). */
50
+ minImportance: number;
51
+ /** Cosine similarity threshold for greedy clustering (default 0.82). */
52
+ clusterThreshold: number;
53
+ /** Max events fetched from the store (default 2000). */
54
+ listLimit: number;
55
+ /** When true (default), find candidates without writing anything. */
56
+ dryRun: boolean;
57
+ }
58
+
59
+ export const DEFAULT_PROMOTE_SCAN_CONFIG: PromoteScanConfig = {
60
+ minOccurrences: 3,
61
+ minImportance: 0.6,
62
+ clusterThreshold: 0.82,
63
+ listLimit: 2000,
64
+ dryRun: true,
65
+ };
66
+
67
+ export interface PromoteCandidate {
68
+ downgradedFrom: PromotableDowngrade;
69
+ seedId: string;
70
+ seedText: string;
71
+ memberIds: string[];
72
+ occurrences: number;
73
+ avgImportance: number;
74
+ /** Filled after a real promotion; null in dryRun mode. */
75
+ promoted: StoredPromotedMemoryRecord | null;
76
+ }
77
+
78
+ export interface PromoteScanResult {
79
+ candidates: PromoteCandidate[];
80
+ scannedEvidence: number;
81
+ clusters: number;
82
+ promoted: number;
83
+ dryRun: boolean;
84
+ }
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // Helpers
88
+ // ---------------------------------------------------------------------------
89
+
90
+ function hasPromotedToMarker(metadata?: string): boolean {
91
+ const parsed = parseMetadataObject(metadata);
92
+ return typeof parsed?.promotedTo === "string" && parsed.promotedTo.trim().length > 0;
93
+ }
94
+
95
+ function resolveDowngrade(entry: MemoryEntry): PromotableDowngrade | null {
96
+ const from = extractBoundaryMetadata(entry.metadata)?.downgradedFrom;
97
+ return from === "profile" || from === "preferences" ? from : null;
98
+ }
99
+
100
+ function averageImportance(members: MemoryEntry[]): number {
101
+ if (members.length === 0) return 0;
102
+ return members.reduce((sum, m) => sum + (m.importance ?? 0), 0) / members.length;
103
+ }
104
+
105
+ /** Pick the highest-importance member as the promotion seed. */
106
+ function pickSeed(members: MemoryEntry[]): MemoryEntry {
107
+ return members.reduce((best, m) => (m.importance > best.importance ? m : best), members[0]);
108
+ }
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // Main scan
112
+ // ---------------------------------------------------------------------------
113
+
114
+ export async function scanMemoryPromotions(
115
+ deps: PromoteScanDeps,
116
+ scope: string,
117
+ config?: Partial<PromoteScanConfig>,
118
+ ): Promise<PromoteScanResult> {
119
+ const cfg: PromoteScanConfig = { ...DEFAULT_PROMOTE_SCAN_CONFIG, ...config };
120
+
121
+ // 1. Load downgraded evidence (events). list() returns empty vectors.
122
+ const events = await deps.store.list([scope], "events", cfg.listLimit, 0);
123
+
124
+ // 2. Keep only active, downgraded-from profile/preferences, not-yet-promoted.
125
+ const downgraded = events.filter(
126
+ (e) =>
127
+ isActiveMemory(e.metadata) &&
128
+ !hasPromotedToMarker(e.metadata) &&
129
+ !isNoise(e.text) &&
130
+ resolveDowngrade(e) !== null,
131
+ );
132
+
133
+ const result: PromoteScanResult = {
134
+ candidates: [],
135
+ scannedEvidence: downgraded.length,
136
+ clusters: 0,
137
+ promoted: 0,
138
+ dryRun: cfg.dryRun,
139
+ };
140
+
141
+ if (downgraded.length === 0) return result;
142
+
143
+ // 3. Backfill vectors (list omits them for performance) and drop the
144
+ // vectorless — they cannot be clustered.
145
+ const vectorMap = await deps.store.getVectors(downgraded.map((e) => e.id));
146
+ const withVectors = downgraded
147
+ .map((e) => ({ ...e, vector: vectorMap.get(e.id) ?? [] }))
148
+ .filter((e) => e.vector.length > 0);
149
+
150
+ // 4. Group by downgraded category — they promote to different targets.
151
+ const groups = new Map<PromotableDowngrade, MemoryEntry[]>();
152
+ for (const entry of withVectors) {
153
+ const from = resolveDowngrade(entry);
154
+ if (!from) continue;
155
+ const bucket = groups.get(from);
156
+ if (bucket) bucket.push(entry);
157
+ else groups.set(from, [entry]);
158
+ }
159
+
160
+ // 5. Cluster each group; qualify candidates by occurrence + importance.
161
+ for (const [downgradedFrom, members] of groups) {
162
+ const clusters = greedyCluster(members, cfg.clusterThreshold);
163
+ result.clusters += clusters.length;
164
+
165
+ for (const cluster of clusters) {
166
+ if (cluster.members.length < cfg.minOccurrences) continue;
167
+ const avgImportance = averageImportance(cluster.members);
168
+ if (avgImportance < cfg.minImportance) continue;
169
+
170
+ const seed = pickSeed(cluster.members);
171
+ const candidate: PromoteCandidate = {
172
+ downgradedFrom,
173
+ seedId: seed.id,
174
+ seedText: seed.text,
175
+ memberIds: cluster.members.map((m) => m.id),
176
+ occurrences: cluster.members.length,
177
+ avgImportance,
178
+ promoted: null,
179
+ };
180
+
181
+ // 6. Promote (unless dry-run). Dedup is handled by promoteMemory.
182
+ if (!cfg.dryRun) {
183
+ candidate.promoted = await deps.promote({
184
+ memoryId: seed.id,
185
+ category: downgradedFrom,
186
+ scope,
187
+ importance: avgImportance,
188
+ });
189
+ result.promoted += 1;
190
+ }
191
+
192
+ result.candidates.push(candidate);
193
+ }
194
+ }
195
+
196
+ return result;
197
+ }
198
+
199
+ // ---------------------------------------------------------------------------
200
+ // Production wiring
201
+ // ---------------------------------------------------------------------------
202
+
203
+ /** Build scan deps from the persist pipeline, wrapping promoteMemory. */
204
+ export function buildPromoteScanDeps(
205
+ deps: PersistMemoryDeps & {
206
+ store: PersistMemoryDeps["store"] & Pick<MemoryStore, "list" | "getVectors">;
207
+ },
208
+ ): PromoteScanDeps {
209
+ return {
210
+ store: deps.store,
211
+ promote: (req) => promoteMemory(deps, req),
212
+ };
213
+ }
214
+
215
+ // ---------------------------------------------------------------------------
216
+ // Formatting
217
+ // ---------------------------------------------------------------------------
218
+
219
+ export function formatPromoteScanResult(result: PromoteScanResult): string {
220
+ const lines = [
221
+ `Promote-scan: ${result.scannedEvidence} downgraded evidence, ${result.clusters} cluster(s) scanned.`,
222
+ ];
223
+ if (result.candidates.length === 0) {
224
+ lines.push("No promotion candidates found.");
225
+ return lines.join("\n");
226
+ }
227
+ lines.push(
228
+ result.dryRun
229
+ ? `Found ${result.candidates.length} candidate(s) (dry-run, nothing written):\n`
230
+ : `Promoted ${result.promoted} of ${result.candidates.length} candidate(s):\n`,
231
+ );
232
+ for (const [i, c] of result.candidates.entries()) {
233
+ lines.push(`### ${i + 1}. [${c.downgradedFrom}] ${c.seedText.split("\n")[0].slice(0, 80)}`);
234
+ lines.push(`Occurrences: ${c.occurrences} | avg importance: ${c.avgImportance.toFixed(2)}`);
235
+ lines.push(`Seed: ${c.seedId.slice(0, 8)} | members: ${c.memberIds.length}`);
236
+ if (c.promoted) {
237
+ lines.push(
238
+ c.promoted.disposition === "conflict" && c.promoted.conflictId
239
+ ? `-> conflict ${c.promoted.conflictId.slice(0, 8)} (manual review)`
240
+ : `-> durable ${c.promoted.id.slice(0, 8)} (${c.promoted.disposition}, key ${c.promoted.canonicalKey})`,
241
+ );
242
+ }
243
+ lines.push("");
244
+ }
245
+ return lines.join("\n");
246
+ }
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
 
3
3
  import { boundedStringSchema, identifierSchema, normalizedStringListSchema, optionalBoundedStringSchema } from "./schema-utils.js";
4
+ import * as envConfig from "./env-config.js";
4
5
 
5
6
  export const DURABLE_MEMORY_CATEGORIES = [
6
7
  "profile",
@@ -130,12 +131,16 @@ export const StoreMemoryInputSchema = z.object({
130
131
  privacyTier: PrivacyTierSchema.default("durable"),
131
132
  });
132
133
 
134
+ // Keep in sync with RejectionReason in admission-control.ts
135
+ export const RejectionReasonSchema = z.enum(["text_too_short", "noise_detected", "importance_too_low", "rate_limited"]);
136
+
133
137
  export const StoredMemoryRecordSchema = StoreMemoryInputSchema.extend({
134
138
  id: identifierSchema("id", 128),
135
139
  resolvedScope: identifierSchema("resolvedScope", 160),
136
140
  storedAt: z.string().datetime(),
137
141
  disposition: WriteDispositionSchema.default("stored"),
138
142
  conflictId: identifierSchema("conflictId", 128).optional(),
143
+ rejectionReason: RejectionReasonSchema.optional(),
139
144
  });
140
145
 
141
146
  export const CaptureMemoryItemSchema = z.object({
@@ -181,6 +186,23 @@ export const WorkflowPatternInputSchema = z.object({
181
186
  canonicalKey: CanonicalKeySchema,
182
187
  });
183
188
 
189
+ /** A1 · break-loop 五维(Trellis 借鉴):debug case 的结构化归因,全可选、向后兼容。 */
190
+ export const DebugFramingSchema = z.object({
191
+ rootCause: z.enum([
192
+ "missing_spec",
193
+ "cross_layer_contract",
194
+ "change_propagation",
195
+ "test_coverage_gap",
196
+ "implicit_assumption",
197
+ ]).describe("根因归类:缺spec/跨层契约/改动传播失败/测试覆盖缺口/隐式假设"),
198
+ whyPriorFixFailed: z.string().max(300).optional().describe("为何之前没修对"),
199
+ defense: z.string().max(300).optional().describe("防御机制(强度:架构>编译期>运行期>测试>review>文档)"),
200
+ systematicExtension: z.string().max(300).optional().describe("系统性扩展:同类问题还在哪"),
201
+ knowledgeFix: z.string().max(300).optional().describe("知识固化:沉淀去向"),
202
+ });
203
+
204
+ export type DebugFraming = z.infer<typeof DebugFramingSchema>;
205
+
184
206
  export const CaseMemoryInputSchema = z.object({
185
207
  title: CaseMemoryTitleSchema,
186
208
  problem: CaseMemoryProblemSchema,
@@ -193,6 +215,7 @@ export const CaseMemoryInputSchema = z.object({
193
215
  source: StoreMemorySourceSchema.default("agent"),
194
216
  tags: MemoryTagsSchema,
195
217
  canonicalKey: CanonicalKeySchema,
218
+ debugFraming: DebugFramingSchema.optional(),
196
219
  });
197
220
 
198
221
  export const PromoteMemoryInputSchema = z.object({
@@ -248,9 +271,9 @@ export type PromoteMemoryInput = z.infer<typeof PromoteMemoryInputSchema>;
248
271
  export type StoredPromotedMemoryRecord = z.infer<typeof StoredPromotedMemoryRecordSchema>;
249
272
 
250
273
  export function isEmotionScoringEnabled(): boolean {
251
- return process.env.RECALLNEST_EMOTION_SCORING === "true";
274
+ return envConfig.emotionScoring();
252
275
  }
253
276
 
254
277
  export function isPredictiveMemoryEnabled(): boolean {
255
- return process.env.RECALLNEST_PREDICTIVE_MEMORY === "true";
278
+ return envConfig.predictiveMemory();
256
279
  }
@@ -0,0 +1,43 @@
1
+ import type { MemoryEntry, MemorySearchResult } from "./store.js";
2
+
3
+ export interface MemoryStoreStats {
4
+ totalCount: number;
5
+ scopeCounts: Record<string, number>;
6
+ categoryCounts: Record<string, number>;
7
+ }
8
+
9
+ export type MemoryStoreUpdate = {
10
+ text?: string;
11
+ vector?: number[];
12
+ importance?: number;
13
+ category?: MemoryEntry["category"];
14
+ metadata?: string;
15
+ timestamp?: number;
16
+ language?: string;
17
+ fts_text?: string;
18
+ };
19
+
20
+ export interface MemoryStorePort {
21
+ stats(scopeFilter?: string[]): Promise<MemoryStoreStats>;
22
+ list(scopeFilter?: string[], category?: string, limit?: number, offset?: number): Promise<MemoryEntry[]>;
23
+ /** DB 层真分页(where/limit/offset 下推),全库维护扫描用;list() 是全量拉取再切片。 */
24
+ listPage(opts?: {
25
+ scopeFilter?: string[];
26
+ category?: string;
27
+ limit?: number;
28
+ offset?: number;
29
+ includeVector?: boolean;
30
+ }): Promise<MemoryEntry[]>;
31
+ get(id: string, scopeFilter?: string[]): Promise<MemoryEntry | null>;
32
+ getById(id: string): Promise<MemoryEntry | null>;
33
+ store(entry: Omit<MemoryEntry, "id" | "timestamp"> & { id?: string }): Promise<MemoryEntry>;
34
+ update(id: string, updates: MemoryStoreUpdate, scopeFilter?: string[]): Promise<MemoryEntry | null>;
35
+ /** metadata 读改写的单写通道(per-id 串行队列)。新代码改 metadata 一律走这里,
36
+ * 不要 getById+update 裸 RMW(并发覆盖)。 */
37
+ patchMetadata(
38
+ id: string,
39
+ patchFn: (meta: Record<string, unknown>, entry: MemoryEntry) => Record<string, unknown>,
40
+ scopeFilter?: string[],
41
+ ): Promise<MemoryEntry | null>;
42
+ vectorSearch(vector: number[], limit?: number, minScore?: number, scopeFilter?: string[]): Promise<MemorySearchResult[]>;
43
+ }
@@ -16,13 +16,14 @@
16
16
  */
17
17
 
18
18
  import type { Embedder } from "./embedder.js";
19
+ import * as envConfig from "./env-config.js";
19
20
 
20
21
  // ---------------------------------------------------------------------------
21
22
  // Config
22
23
  // ---------------------------------------------------------------------------
23
24
 
24
25
  export function isMultiVectorEnabled(): boolean {
25
- return process.env.RECALLNEST_MULTI_VECTOR === "true";
26
+ return envConfig.multiVector();
26
27
  }
27
28
 
28
29
  // ---------------------------------------------------------------------------
@@ -12,13 +12,14 @@
12
12
  */
13
13
 
14
14
  import { z } from "zod";
15
+ import * as envConfig from "./env-config.js";
15
16
 
16
17
  // ---------------------------------------------------------------------------
17
18
  // Feature Flag
18
19
  // ---------------------------------------------------------------------------
19
20
 
20
21
  export function isNarrativeModeEnabled(): boolean {
21
- return process.env.RECALLNEST_NARRATIVE_MODE === "true";
22
+ return envConfig.narrativeMode();
22
23
  }
23
24
 
24
25
  // ---------------------------------------------------------------------------
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import { logInfo } from "./stderr-log.js";
8
+ import { redactForLog } from "./pii-detector.js";
8
9
 
9
10
  // Agent-side denial patterns
10
11
  const DENIAL_PATTERNS = [
@@ -45,7 +46,7 @@ const META_QUESTION_PATTERNS = [
45
46
 
46
47
  // Session boilerplate (anchored to prevent false positives on real content)
47
48
  const BOILERPLATE_PATTERNS = [
48
- /^(hi|hello|hey|good morning|good evening|greetings)/i,
49
+ /^(hi|hello|hey|good morning|good evening|greetings)\b/i,
49
50
  /^fresh session/i,
50
51
  /^new session/i,
51
52
  /^HEARTBEAT/i,
@@ -81,6 +82,43 @@ const DIAGNOSTIC_ARTIFACT_PATTERNS = [
81
82
  /^只回复精确代号/i,
82
83
  ];
83
84
 
85
+ // CC bridge injected agent prompts: the Telegram bridge writes CC's own system
86
+ // prompts (reply / self-decision templates) as `user` turns, which transcript
87
+ // ingest then mis-extracts as profile/preferences. They are never user content.
88
+ const AGENT_PROMPT_PATTERNS = [
89
+ /^#{1,3}\s*CC\s+(Reply|Self-Decision)\s+Prompt\b/im,
90
+ /你不是\s*Alice\s*的数字分身/,
91
+ /你是\s*CC[。.]?\s*(Alice\s*刚给你发了消息|现在你周期性醒来)/,
92
+ // Fixed sections of the bridge prompt that survive ingest as standalone chunks
93
+ // (the long prompt turn gets split, so the title/persona lines may be absent).
94
+ /你的输出\s*[\r\n]+\s*直接回复\s*Alice/,
95
+ /节奏要求[::]\s*[\r\n]+\s*[-*]\s*短话题/,
96
+ ];
97
+
98
+ // CC bridge self-decision / RecallNest distill payloads embed structured JSON
99
+ // (decision logs, conversation history, interaction stats). Long prompts get
100
+ // split by transcript chunking, orphaning JSON field-lines that lost their
101
+ // enclosing braces — these survive ingest looking like preferences. We match
102
+ // the severed-fragment SHAPE, not JSON in general: a complete { } object is left
103
+ // alone (it may be real discussion); only orphaned field-lines (start as "key":
104
+ // with no brace) and dangling structural tokens (leading , } ]) qualify.
105
+ const CONTEXT_JSON_FRAGMENT_PATTERNS = [
106
+ // bridge/distill proprietary schema keys — ANCHORED to line start (^). These
107
+ // keys are also RecallNest's own schema fields, so a real memory may MENTION
108
+ // them inline (e.g. 我们讨论 "distilled_at": 要不要保留); anchoring ensures only
109
+ // an orphaned line that LEADS with the key is filtered, not such discussion.
110
+ /^"?(?:next_check_hint|alice_interaction_stats(?:_7d)?|alice_last_cc_hours_ago|engagement_rate|seen_no_reply|distilled_at)"\s*:/,
111
+ // orphan field-line: whole chunk starts as "key": <scalar> with no { wrapper.
112
+ // SCALAR values only (string/number/bool/null). A `{` or `[` value is left
113
+ // alone so real config / package.json chunks like `"scripts": {` survive.
114
+ /^"?[A-Za-z_][\w]*"\s*:\s*(?:"|\d|true\b|false\b|null\b)/,
115
+ // severed JSON array/object element: a dangling , } ] then a NEWLINE into a
116
+ // brace/quoted-key. Requiring the newline-then-JSON shape avoids real code
117
+ // fragments like `} catch (e) {` or `});` (close-token followed by code on
118
+ // the same line, never a newline into a JSON key).
119
+ /^[,}\]]+[ \t]*\r?\n\s*[{[]?\s*"[A-Za-z_]/,
120
+ ];
121
+
84
122
  export interface NoiseFilterOptions {
85
123
  /** Filter agent denial responses (default: true) */
86
124
  filterDenials?: boolean;
@@ -88,12 +126,15 @@ export interface NoiseFilterOptions {
88
126
  filterMetaQuestions?: boolean;
89
127
  /** Filter session boilerplate (default: true) */
90
128
  filterBoilerplate?: boolean;
129
+ /** Filter CC bridge injected agent prompts (default: true) */
130
+ filterAgentPrompts?: boolean;
91
131
  }
92
132
 
93
133
  const DEFAULT_OPTIONS: Required<NoiseFilterOptions> = {
94
134
  filterDenials: true,
95
135
  filterMetaQuestions: true,
96
136
  filterBoilerplate: true,
137
+ filterAgentPrompts: true,
97
138
  };
98
139
 
99
140
  /**
@@ -110,21 +151,21 @@ export function isNoise(text: string, options: NoiseFilterOptions = {}): boolean
110
151
  }
111
152
 
112
153
  if (opts.filterDenials && DENIAL_PATTERNS.some(p => p.test(trimmed))) {
113
- logInfo(`[INFO] noise-filter: denial pattern matched: "${trimmed.slice(0, 60)}..."`);
154
+ logInfo(`[INFO] noise-filter: denial pattern matched: "${redactForLog(trimmed)}..."`);
114
155
  return true;
115
156
  }
116
157
  if (opts.filterMetaQuestions && META_QUESTION_PATTERNS.some(p => p.test(trimmed))) {
117
- logInfo(`[INFO] noise-filter: meta-question filtered: "${trimmed.slice(0, 60)}..."`);
158
+ logInfo(`[INFO] noise-filter: meta-question filtered: "${redactForLog(trimmed)}..."`);
118
159
  return true;
119
160
  }
120
161
  if (opts.filterBoilerplate) {
121
162
  if (BOILERPLATE_PATTERNS.some(p => p.test(trimmed))) {
122
- logInfo(`[INFO] noise-filter: boilerplate filtered: "${trimmed.slice(0, 60)}..."`);
163
+ logInfo(`[INFO] noise-filter: boilerplate filtered: "${redactForLog(trimmed)}..."`);
123
164
  return true;
124
165
  }
125
166
  if (trimmed.length <= BOILERPLATE_MAX_LENGTH &&
126
167
  SHORT_BOILERPLATE_PATTERNS.some(p => p.test(trimmed))) {
127
- logInfo(`[INFO] noise-filter: short boilerplate filtered: "${trimmed}"`);
168
+ logInfo(`[INFO] noise-filter: short boilerplate filtered: "${redactForLog(trimmed)}"`);
128
169
  return true;
129
170
  }
130
171
  }
@@ -135,7 +176,17 @@ export function isNoise(text: string, options: NoiseFilterOptions = {}): boolean
135
176
  }
136
177
  // Diagnostic artifact noise (synced from upstream v1.1.0-beta.3)
137
178
  if (DIAGNOSTIC_ARTIFACT_PATTERNS.some(p => p.test(trimmed))) {
138
- logInfo(`[INFO] noise-filter: diagnostic artifact filtered: "${trimmed.slice(0, 60)}..."`);
179
+ logInfo(`[INFO] noise-filter: diagnostic artifact filtered: "${redactForLog(trimmed)}..."`);
180
+ return true;
181
+ }
182
+ // CC bridge injected agent prompts (reply/self-decision templates leaked as user turns)
183
+ if (opts.filterAgentPrompts && AGENT_PROMPT_PATTERNS.some(p => p.test(trimmed))) {
184
+ logInfo(`[INFO] noise-filter: agent prompt filtered: "${redactForLog(trimmed)}..."`);
185
+ return true;
186
+ }
187
+ // Severed bridge/distill context-JSON fragments (orphaned field-lines from chunking)
188
+ if (opts.filterAgentPrompts && CONTEXT_JSON_FRAGMENT_PATTERNS.some(p => p.test(trimmed))) {
189
+ logInfo(`[INFO] noise-filter: context JSON fragment filtered: "${redactForLog(trimmed)}..."`);
139
190
  return true;
140
191
  }
141
192