recallnest 2.3.0 → 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 (221) hide show
  1. package/CHANGELOG.md +150 -0
  2. package/README.md +17 -31
  3. package/README_CN.md +12 -22
  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 +22 -1
  15. package/src/__tests__/access-tracker-novelty.test.ts +238 -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__/auto-consolidation.test.ts +168 -0
  21. package/src/__tests__/auto-recall.test.ts +131 -0
  22. package/src/__tests__/babel-integration.test.ts +144 -0
  23. package/src/__tests__/batch-dedup.test.ts +65 -0
  24. package/src/__tests__/canary-eval.test.ts +114 -0
  25. package/src/__tests__/capture-engine.test.ts +1238 -0
  26. package/src/__tests__/capture-heuristic.test.ts +171 -0
  27. package/src/__tests__/cascade-forget.test.ts +210 -0
  28. package/src/__tests__/category-min-scores.test.ts +33 -0
  29. package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
  30. package/src/__tests__/circuit-breaker.test.ts +99 -0
  31. package/src/__tests__/cluster-consolidation.test.ts +782 -0
  32. package/src/__tests__/confidence-metadata.test.ts +125 -0
  33. package/src/__tests__/confidence-tracker.test.ts +261 -0
  34. package/src/__tests__/conflict-advisor.test.ts +206 -0
  35. package/src/__tests__/conflict-escalation.test.ts +149 -0
  36. package/src/__tests__/conflict-lifecycle.test.ts +83 -0
  37. package/src/__tests__/conflict-output.test.ts +85 -0
  38. package/src/__tests__/conflict-store.test.ts +374 -0
  39. package/src/__tests__/connector-v1.test.ts +181 -0
  40. package/src/__tests__/consolidation-engine.test.ts +333 -0
  41. package/src/__tests__/constructive-retrieval.test.ts +509 -0
  42. package/src/__tests__/context-collapse-renderer.test.ts +214 -0
  43. package/src/__tests__/context-composer-pins.test.ts +64 -0
  44. package/src/__tests__/context-composer-task-selection.test.ts +24 -0
  45. package/src/__tests__/context-reconstructor.test.ts +65 -0
  46. package/src/__tests__/context-renderer.test.ts +91 -0
  47. package/src/__tests__/continuity-eval.test.ts +275 -0
  48. package/src/__tests__/continuity-schema.test.ts +97 -0
  49. package/src/__tests__/contracts.test.ts +216 -0
  50. package/src/__tests__/conversation-importer.test.ts +692 -0
  51. package/src/__tests__/core-recall-eval.test.ts +235 -0
  52. package/src/__tests__/core-summary.test.ts +109 -0
  53. package/src/__tests__/data-checkup.test.ts +276 -0
  54. package/src/__tests__/decay-exemption.test.ts +84 -0
  55. package/src/__tests__/dedup-actions.test.ts +58 -0
  56. package/src/__tests__/dedup-guard.test.ts +39 -0
  57. package/src/__tests__/deterministic-id.test.ts +39 -0
  58. package/src/__tests__/distill-lock.test.ts +305 -0
  59. package/src/__tests__/distill-session-argshape.test.ts +79 -0
  60. package/src/__tests__/doctor.test.ts +319 -0
  61. package/src/__tests__/dream-pipeline.test.ts +321 -0
  62. package/src/__tests__/drill-down.test.ts +108 -0
  63. package/src/__tests__/embedder.test.ts +122 -0
  64. package/src/__tests__/emotion-decay.test.ts +46 -0
  65. package/src/__tests__/emotion-detector.test.ts +56 -0
  66. package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
  67. package/src/__tests__/emotion-retrieval.test.ts +56 -0
  68. package/src/__tests__/entity-resolver.test.ts +58 -0
  69. package/src/__tests__/env-config.test.ts +151 -0
  70. package/src/__tests__/error-signature-boost.test.ts +87 -0
  71. package/src/__tests__/error-signature.test.ts +95 -0
  72. package/src/__tests__/error-taxonomy.test.ts +129 -0
  73. package/src/__tests__/ethics-layer.test.ts +476 -0
  74. package/src/__tests__/event-segmenter.test.ts +153 -0
  75. package/src/__tests__/feature-flags-integration.test.ts +54 -0
  76. package/src/__tests__/frequency-tracker.test.ts +113 -0
  77. package/src/__tests__/graph-export.test.ts +489 -0
  78. package/src/__tests__/hotness-blend.test.ts +76 -0
  79. package/src/__tests__/ingest-chunking.test.ts +44 -0
  80. package/src/__tests__/ingest.test.ts +360 -0
  81. package/src/__tests__/interference-detector.test.ts +203 -0
  82. package/src/__tests__/kg-extractor.test.ts +192 -0
  83. package/src/__tests__/kg-store.test.ts +300 -0
  84. package/src/__tests__/llm-consolidation.test.ts +226 -0
  85. package/src/__tests__/maintenance-locks.test.ts +174 -0
  86. package/src/__tests__/mcp-efficiency.test.ts +288 -0
  87. package/src/__tests__/mcp-related-scopes.test.ts +151 -0
  88. package/src/__tests__/memory-boundaries.test.ts +250 -0
  89. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +345 -0
  90. package/src/__tests__/memory-evolution.test.ts +271 -0
  91. package/src/__tests__/memory-health-rebalance.test.ts +144 -0
  92. package/src/__tests__/memory-lint.test.ts +421 -0
  93. package/src/__tests__/memory-output.test.ts +215 -0
  94. package/src/__tests__/memory-promotion.test.ts +315 -0
  95. package/src/__tests__/migrate-dedup-ids.test.ts +65 -0
  96. package/src/__tests__/multi-vector.test.ts +242 -0
  97. package/src/__tests__/narrative-architecture.test.ts +456 -0
  98. package/src/__tests__/noise-filter.test.ts +205 -0
  99. package/src/__tests__/obsidian-connector.test.ts +363 -0
  100. package/src/__tests__/omitted-reporting.test.ts +98 -0
  101. package/src/__tests__/pending-review.test.ts +77 -0
  102. package/src/__tests__/pii-redact.test.ts +314 -0
  103. package/src/__tests__/post-retrieval-filter.test.ts +178 -0
  104. package/src/__tests__/ppr-traversal.test.ts +256 -0
  105. package/src/__tests__/prediction-engine.test.ts +349 -0
  106. package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
  107. package/src/__tests__/preference-guard-smoke.test.ts +211 -0
  108. package/src/__tests__/preference-matcher.test.ts +230 -0
  109. package/src/__tests__/preference-slots.test.ts +205 -0
  110. package/src/__tests__/prospective-memory.test.ts +283 -0
  111. package/src/__tests__/query-entity-detector.test.ts +171 -0
  112. package/src/__tests__/query-expander-alias.test.ts +146 -0
  113. package/src/__tests__/query-expander.test.ts +34 -0
  114. package/src/__tests__/recall-governor.test.ts +227 -0
  115. package/src/__tests__/recall-mode.test.ts +64 -0
  116. package/src/__tests__/result-synthesizer.test.ts +198 -0
  117. package/src/__tests__/resume-context.test.ts +4898 -0
  118. package/src/__tests__/retrieval-trace.test.ts +213 -0
  119. package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
  120. package/src/__tests__/retroactive-boost.test.ts +223 -0
  121. package/src/__tests__/salience-scoring.test.ts +106 -0
  122. package/src/__tests__/scope-inventory.test.ts +200 -0
  123. package/src/__tests__/scope-policy.test.ts +69 -0
  124. package/src/__tests__/scope-suggester.test.ts +80 -0
  125. package/src/__tests__/session-checkpoint.test.ts +291 -0
  126. package/src/__tests__/session-distiller.test.ts +640 -0
  127. package/src/__tests__/session-gc.test.ts +215 -0
  128. package/src/__tests__/skill-engine.test.ts +552 -0
  129. package/src/__tests__/skill-promotion.test.ts +601 -0
  130. package/src/__tests__/skill-verifier.test.ts +122 -0
  131. package/src/__tests__/source-heartbeat.test.ts +250 -0
  132. package/src/__tests__/store-patch-metadata.test.ts +205 -0
  133. package/src/__tests__/store-scope-audit.test.ts +81 -0
  134. package/src/__tests__/store-upsert.test.ts +184 -0
  135. package/src/__tests__/supersede-chain.test.ts +154 -0
  136. package/src/__tests__/temporal-parser.test.ts +241 -0
  137. package/src/__tests__/temporal-validity.test.ts +160 -0
  138. package/src/__tests__/term-registry.test.ts +121 -0
  139. package/src/__tests__/tool-error-surfacing.test.ts +98 -0
  140. package/src/__tests__/tool-output-compressor.test.ts +160 -0
  141. package/src/__tests__/topic-tag.test.ts +113 -0
  142. package/src/__tests__/trusted-memory.test.ts +839 -0
  143. package/src/__tests__/usage-tracker.test.ts +287 -0
  144. package/src/__tests__/version-manager.test.ts +213 -0
  145. package/src/__tests__/workflow-observation-skill-outcome.test.ts +352 -0
  146. package/src/__tests__/workflow-observation.test.ts +433 -0
  147. package/src/__tests__/write-verifier.test.ts +122 -0
  148. package/src/access-tracker.ts +38 -32
  149. package/src/activity-counter.ts +46 -18
  150. package/src/aliases.ts +133 -0
  151. package/src/api-server.ts +27 -7
  152. package/src/audit-log.ts +4 -2
  153. package/src/auto-consolidation.ts +8 -3
  154. package/src/auto-gc.ts +85 -21
  155. package/src/capture-engine.ts +54 -21
  156. package/src/cli.ts +169 -37
  157. package/src/confidence-tracker.ts +36 -18
  158. package/src/consolidation-engine.ts +109 -11
  159. package/src/context-composer.ts +8 -1
  160. package/src/data-checkup.ts +95 -6
  161. package/src/distill-lock.ts +194 -15
  162. package/src/doctor.ts +9 -2
  163. package/src/dream-pipeline.ts +118 -34
  164. package/src/env-config.ts +77 -0
  165. package/src/error-signature.ts +153 -0
  166. package/src/error-taxonomy.ts +115 -0
  167. package/src/eval.ts +251 -6
  168. package/src/idempotency.ts +8 -0
  169. package/src/ingest.ts +31 -10
  170. package/src/kg-extractor.ts +6 -2
  171. package/src/kg-store.ts +236 -44
  172. package/src/llm-consolidation.ts +2 -1
  173. package/src/mcp-server.ts +85 -1799
  174. package/src/mcp-tool-deps.ts +48 -0
  175. package/src/mcp-tools-advanced.ts +941 -0
  176. package/src/mcp-tools-core.ts +633 -0
  177. package/src/mcp-tools-governance.ts +475 -0
  178. package/src/memory-boundaries.ts +3 -1
  179. package/src/memory-evolution.ts +73 -1
  180. package/src/memory-lint.ts +183 -10
  181. package/src/memory-output.ts +114 -3
  182. package/src/memory-promotion.ts +246 -0
  183. package/src/memory-schema.ts +25 -2
  184. package/src/memory-store-port.ts +43 -0
  185. package/src/multi-vector.ts +2 -1
  186. package/src/narrative-schema.ts +2 -1
  187. package/src/noise-filter.ts +57 -6
  188. package/src/pii-detector.ts +157 -5
  189. package/src/query-expander.ts +90 -9
  190. package/src/result-synthesizer.ts +2 -1
  191. package/src/retention-policy.ts +2 -1
  192. package/src/retriever.ts +259 -139
  193. package/src/runtime-config.ts +8 -1
  194. package/src/scope-suggester.ts +113 -0
  195. package/src/session-distiller.ts +23 -2
  196. package/src/session-engine.ts +2 -1
  197. package/src/session-schema.ts +1 -0
  198. package/src/session-store.ts +5 -0
  199. package/src/skill-engine.ts +78 -0
  200. package/src/skill-promotion.ts +247 -47
  201. package/src/skill-schema.ts +16 -3
  202. package/src/skill-verifier.ts +152 -0
  203. package/src/store.ts +267 -38
  204. package/src/tool-output-compressor.ts +3 -1
  205. package/src/ui-server.ts +2 -1
  206. package/src/usage-tracker.ts +242 -0
  207. package/src/version-manager.ts +3 -2
  208. package/src/workflow-observation-engine.ts +2 -1
  209. package/src/workflow-observation-schema.ts +2 -1
  210. package/src/workflow-observation-store.ts +6 -2
  211. package/.env +0 -4
  212. package/CLAUDE.md +0 -46
  213. package/config.json +0 -55
  214. package/daily-recall.sh +0 -110
  215. package/logs/daily-recall.error.log +0 -0
  216. package/logs/daily-recall.log +0 -60
  217. package/logs/ingest-2026-04-11.log +0 -110
  218. package/logs/ingest-2026-04-12.log +0 -90
  219. package/logs/ingest-2026-04-13.log +0 -87
  220. package/logs/launchagent-stderr.log +0 -0
  221. package/logs/launchagent-stdout.log +0 -0
@@ -2,12 +2,21 @@
2
2
  * F-3: Lightweight PII detection.
3
3
  * Scans text for common sensitive patterns (passwords, tokens, IDs).
4
4
  * Returns warnings, does not block writes.
5
+ *
6
+ * F-3b (2026-07-10): Deterministic secret redaction on the same rule set.
7
+ * `redactSecrets` scrubs redactable matches BEFORE text reaches any external
8
+ * surface (LLM prompts, embedding API, stderr logs, audit trail). Detection
9
+ * and redaction share one rule table so the two can never drift apart.
10
+ * Deliberately NOT redacted: phone / email (legitimate memory content, warn
11
+ * only) and bare long-hex (would false-positive on git commit hashes, which
12
+ * are high-value memory content; prefixed forms like `secret=<hex>` are
13
+ * caught by the assignment rules).
5
14
  */
6
15
 
7
16
  export type PIISeverity = "high" | "medium" | "low";
8
17
 
9
18
  export interface PIIDetection {
10
- type: string; // "api_key" | "password" | "id_number" | "email" | "phone" | "credit_card"
19
+ type: string; // "api_key" | "password" | "id_number" | "email" | "phone" | "credit_card" | ...
11
20
  severity: PIISeverity;
12
21
  match: string; // the matched text (partially masked)
13
22
  position: number; // char offset
@@ -19,42 +28,147 @@ export interface PIIScanResult {
19
28
  summary: string; // human-readable summary
20
29
  }
21
30
 
31
+ export interface RedactResult {
32
+ text: string; // scrubbed text
33
+ redacted: number; // number of replacements made
34
+ }
35
+
22
36
  interface PIIRule {
23
37
  type: string;
24
38
  severity: PIISeverity;
25
39
  pattern: RegExp;
40
+ /** Scrub matches via redactSecrets (detection-only rules leave text intact) */
41
+ redact: boolean;
42
+ /** For key=value style rules: keep the key name, scrub only the value */
43
+ keepPrefix?: RegExp;
44
+ /** Checksum gate for broad numeric patterns: redact ONLY when this returns
45
+ * true (scan still warns regardless). Keeps snowflake IDs / order numbers
46
+ * from being destructively rewritten. */
47
+ validate?: (match: string) => boolean;
48
+ }
49
+
50
+ /** GB 11643 checksum (ISO 7064 MOD 11-2) for 18-digit Chinese national IDs. */
51
+ function isValidChineseId(id: string): boolean {
52
+ if (!/^\d{17}[\dXx]$/.test(id)) return false;
53
+ const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
54
+ const checkChars = "10X98765432";
55
+ let sum = 0;
56
+ for (let i = 0; i < 17; i++) sum += Number(id[i]) * weights[i];
57
+ return checkChars[sum % 11] === id[17].toUpperCase();
58
+ }
59
+
60
+ /** Luhn checksum for card-number candidates (separators stripped first). */
61
+ function isValidLuhn(candidate: string): boolean {
62
+ const digits = candidate.replace(/[\s-]/g, "");
63
+ if (!/^\d{16}$/.test(digits)) return false;
64
+ let sum = 0;
65
+ for (let i = 0; i < 16; i++) {
66
+ let d = Number(digits[15 - i]);
67
+ if (i % 2 === 1) {
68
+ d *= 2;
69
+ if (d > 9) d -= 9;
70
+ }
71
+ sum += d;
72
+ }
73
+ return sum % 10 === 0;
26
74
  }
27
75
 
28
76
  const PII_RULES: PIIRule[] = [
77
+ // --- Vendor-prefixed token literals (high confidence, full scrub) ---
78
+ {
79
+ type: "anthropic_key",
80
+ severity: "high",
81
+ pattern: /sk-ant-[A-Za-z0-9_\-]{10,}/g,
82
+ redact: true,
83
+ },
84
+ {
85
+ type: "github_token",
86
+ severity: "high",
87
+ pattern: /gh[pousr]_[A-Za-z0-9]{20,}/g,
88
+ redact: true,
89
+ },
90
+ {
91
+ type: "slack_token",
92
+ severity: "high",
93
+ pattern: /xox[baprs]-[A-Za-z0-9\-]{10,}/g,
94
+ redact: true,
95
+ },
96
+ {
97
+ type: "aws_access_key",
98
+ severity: "high",
99
+ pattern: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g,
100
+ redact: true,
101
+ },
102
+ {
103
+ type: "google_api_key",
104
+ severity: "high",
105
+ pattern: /\bAIza[A-Za-z0-9_\-]{35}\b/g,
106
+ redact: true,
107
+ },
108
+ {
109
+ type: "jwt",
110
+ severity: "high",
111
+ pattern: /\beyJ[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}\b/g,
112
+ redact: true,
113
+ },
114
+ {
115
+ type: "bearer_token",
116
+ severity: "high",
117
+ pattern: /\bBearer\s+[A-Za-z0-9._~+\/\-]{16,}=*/g,
118
+ redact: true,
119
+ },
120
+ {
121
+ type: "pem_private_key",
122
+ severity: "high",
123
+ pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
124
+ redact: true,
125
+ },
126
+ // --- Generic assignment / key styles (keep key name, scrub value) ---
127
+ // Quoted keys and whitespace around the separator are allowed so JSON/YAML/
128
+ // config forms ({"api_key": "..."}, password = ...) are caught too.
29
129
  {
30
130
  type: "api_key",
31
131
  severity: "high",
32
- pattern: /(?:sk-[A-Za-z0-9_\-]{20,}|(?:api[_-]?key|token|secret)[=:\s]["']?[A-Za-z0-9_\-]{20,})/gi,
132
+ pattern: /(?:sk-[A-Za-z0-9_\-]{20,}|["']?(?:api[_-]?key|token|secret)["']?\s*[=:]\s*["']?[A-Za-z0-9_\-]{20,})/gi,
133
+ redact: true,
134
+ keepPrefix: /^["']?(?:api[_-]?key|token|secret)["']?\s*[=:]\s*["']?/i,
33
135
  },
34
136
  {
35
137
  type: "password",
36
138
  severity: "high",
37
- pattern: /(?:password|passwd|pwd)[=:\s]["']?[^\s"']{8,}/gi,
139
+ pattern: /["']?(?:password|passwd|pwd)["']?\s*[=:]\s*["']?[^\s"',;]{8,}/gi,
140
+ redact: true,
141
+ keepPrefix: /^["']?(?:password|passwd|pwd)["']?\s*[=:]\s*["']?/i,
38
142
  },
143
+ // --- High-risk personal identifiers ---
144
+ // Broad numeric patterns scan-warn on shape but only REDACT when the
145
+ // checksum verifies — otherwise snowflake IDs / order numbers get rewritten.
39
146
  {
40
147
  type: "id_number",
41
148
  severity: "high",
42
- pattern: /\d{17}[\dXx]/g,
149
+ pattern: /(?<!\d)\d{17}[\dXx](?!\d)/g,
150
+ redact: true,
151
+ validate: isValidChineseId,
43
152
  },
44
153
  {
45
154
  type: "credit_card",
46
155
  severity: "high",
47
- pattern: /\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}/g,
156
+ pattern: /(?<!\d)\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}(?![\d])/g,
157
+ redact: true,
158
+ validate: isValidLuhn,
48
159
  },
160
+ // --- Detection-only: legitimate memory content, warn but keep ---
49
161
  {
50
162
  type: "phone",
51
163
  severity: "medium",
52
164
  pattern: /1[3-9]\d{9}/g,
165
+ redact: false,
53
166
  },
54
167
  {
55
168
  type: "email",
56
169
  severity: "low",
57
170
  pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
171
+ redact: false,
58
172
  },
59
173
  ];
60
174
 
@@ -96,3 +210,41 @@ export function scanForPII(text: string): PIIScanResult {
96
210
  summary,
97
211
  };
98
212
  }
213
+
214
+ /**
215
+ * Deterministically scrub redactable secrets. Pure regex, no LLM call.
216
+ * Replacement is `[REDACTED:<type>]`; assignment-style rules keep the key
217
+ * name (`password=[REDACTED:password]`) so the memory stays readable.
218
+ */
219
+ export function redactSecrets(text: string): RedactResult {
220
+ let out = text;
221
+ let redacted = 0;
222
+
223
+ for (const rule of PII_RULES) {
224
+ if (!rule.redact) continue;
225
+ rule.pattern.lastIndex = 0;
226
+ out = out.replace(rule.pattern, (match) => {
227
+ if (rule.validate && !rule.validate(match)) {
228
+ return match; // checksum failed → leave intact (detection-only)
229
+ }
230
+ redacted++;
231
+ if (rule.keepPrefix) {
232
+ const prefixMatch = match.match(rule.keepPrefix);
233
+ if (prefixMatch) {
234
+ return `${prefixMatch[0]}[REDACTED:${rule.type}]`;
235
+ }
236
+ }
237
+ return `[REDACTED:${rule.type}]`;
238
+ });
239
+ }
240
+
241
+ return { text: out, redacted };
242
+ }
243
+
244
+ /**
245
+ * Redact + truncate for log lines. Truncation happens AFTER redaction so a
246
+ * secret can never straddle the cut and leak its prefix.
247
+ */
248
+ export function redactForLog(text: string, max = 60): string {
249
+ return redactSecrets(text).text.slice(0, max);
250
+ }
@@ -10,8 +10,8 @@
10
10
  * v1.3: P0.3 — User-level alias-map (data/alias-map.json) for short query expansion.
11
11
  */
12
12
 
13
- import { existsSync, readFileSync } from "fs";
14
- import { join } from "path";
13
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
14
+ import { dirname, join } from "path";
15
15
  import { resolveQueryEntities } from "./entity-resolver.js";
16
16
 
17
17
  interface SynonymEntry {
@@ -89,9 +89,14 @@ export function resetAliasMapCache(): void {
89
89
  aliasMapCache = null;
90
90
  }
91
91
 
92
+ /** Resolve the alias-map file path currently in effect. */
93
+ export function aliasMapFilePath(): string {
94
+ return aliasMapPath ?? join(process.cwd(), "data", "alias-map.json");
95
+ }
96
+
92
97
  function loadAliasMap(): AliasEntry[] {
93
98
  if (aliasMapCache !== null) return aliasMapCache;
94
- const filePath = aliasMapPath ?? join(process.cwd(), "data", "alias-map.json");
99
+ const filePath = aliasMapFilePath();
95
100
  try {
96
101
  if (existsSync(filePath)) {
97
102
  const raw = JSON.parse(readFileSync(filePath, "utf-8"));
@@ -112,6 +117,75 @@ function loadAliasMap(): AliasEntry[] {
112
117
  return aliasMapCache;
113
118
  }
114
119
 
120
+ // ---------------------------------------------------------------------------
121
+ // User alias management (P1-B manage_alias tool 后端)
122
+ //
123
+ // 这些别名只作用于 BM25 通道(expandQuery 在 hybridRetrieval 内部被调用),
124
+ // 刻意不进 vector embedding——随手加的用户别名若改变语义向量,排查
125
+ // retrieval drift 会非常困难。全通道的 builtin 规则在 src/aliases.ts。
126
+ // ---------------------------------------------------------------------------
127
+
128
+ /** 写入校验:trigger 最短 2 字符(防高频泛词),expansions 最多 8 个。 */
129
+ export const USER_ALIAS_TRIGGER_MIN = 2;
130
+ export const USER_ALIAS_EXPANSIONS_MAX = 8;
131
+
132
+ export type AliasUpsertResult =
133
+ | { ok: true; action: "added" | "updated"; entry: AliasEntry }
134
+ | { ok: false; reason: string };
135
+
136
+ /** 新增/更新用户别名(写 alias-map.json;校验后缓存即时失效)。 */
137
+ export function upsertUserAlias(trigger: string, expansions: string[]): AliasUpsertResult {
138
+ const t = trigger.trim();
139
+ const exp = [...new Set(expansions.map(e => e.trim()).filter(e => e.length > 0))];
140
+
141
+ if (t.length < USER_ALIAS_TRIGGER_MIN) {
142
+ return { ok: false, reason: `trigger 太短(<${USER_ALIAS_TRIGGER_MIN} 字符)——高频泛词会污染所有查询` };
143
+ }
144
+ if (exp.length === 0) {
145
+ return { ok: false, reason: "expansions 为空" };
146
+ }
147
+ if (exp.length > USER_ALIAS_EXPANSIONS_MAX) {
148
+ return { ok: false, reason: `expansions 超过 ${USER_ALIAS_EXPANSIONS_MAX} 个——过长扩展会冲淡 BM25 原查询词权重` };
149
+ }
150
+ if (exp.every(e => e === t)) {
151
+ return { ok: false, reason: "expansions 与 trigger 相同,无扩展意义" };
152
+ }
153
+
154
+ const rules = loadAliasMap();
155
+ const idx = rules.findIndex(r => r.trigger === t);
156
+ const entry: AliasEntry = { trigger: t, expansions: exp };
157
+ const next = idx >= 0 ? rules.map((r, i) => (i === idx ? entry : r)) : [...rules, entry];
158
+
159
+ const path = aliasMapFilePath();
160
+ mkdirSync(dirname(path), { recursive: true });
161
+ writeFileSync(path, JSON.stringify(next, null, 2) + "\n", "utf-8");
162
+ aliasMapCache = null;
163
+ return { ok: true, action: idx >= 0 ? "updated" : "added", entry };
164
+ }
165
+
166
+ /** 删除用户别名;返回是否真的删了。 */
167
+ export function removeUserAlias(trigger: string): boolean {
168
+ const rules = loadAliasMap();
169
+ const next = rules.filter(r => r.trigger !== trigger.trim());
170
+ if (next.length === rules.length) return false;
171
+ const path = aliasMapFilePath();
172
+ mkdirSync(dirname(path), { recursive: true });
173
+ writeFileSync(path, JSON.stringify(next, null, 2) + "\n", "utf-8");
174
+ aliasMapCache = null;
175
+ return true;
176
+ }
177
+
178
+ /** 列出用户别名规则。 */
179
+ export function listUserAliases(): AliasEntry[] {
180
+ return [...loadAliasMap()];
181
+ }
182
+
183
+ /** 检视:query 命中了哪些用户别名(BM25-only 通道)。 */
184
+ export function explainUserAliases(query: string): AliasEntry[] {
185
+ const lower = query.toLowerCase();
186
+ return loadAliasMap().filter(e => lower.includes(e.trigger.toLowerCase()));
187
+ }
188
+
115
189
  /**
116
190
  * Expand a query by appending synonym terms from the dictionary.
117
191
  * Returns the original query with additional terms appended.
@@ -123,7 +197,8 @@ export function expandQuery(query: string): string {
123
197
  // Entity resolution pre-pass: normalize aliases before synonym expansion
124
198
  const resolved = resolveQueryEntities(query);
125
199
  const lower = resolved.toLowerCase();
126
- const additions = new Set<string>();
200
+ const synonymAdditions = new Set<string>();
201
+ const aliasAdditions = new Set<string>();
127
202
 
128
203
  // Static synonym map (built-in)
129
204
  for (const entry of SYNONYM_MAP) {
@@ -138,7 +213,7 @@ export function expandQuery(query: string): string {
138
213
  if (cnMatch || enMatch) {
139
214
  for (const exp of entry.expansions) {
140
215
  if (!lower.includes(exp.toLowerCase())) {
141
- additions.add(exp);
216
+ synonymAdditions.add(exp);
142
217
  }
143
218
  }
144
219
  }
@@ -151,15 +226,21 @@ export function expandQuery(query: string): string {
151
226
  if (lower.includes(triggerLower)) {
152
227
  for (const exp of alias.expansions) {
153
228
  if (!lower.includes(exp.toLowerCase())) {
154
- additions.add(exp);
229
+ aliasAdditions.add(exp);
155
230
  }
156
231
  }
157
232
  }
158
233
  }
159
234
 
160
- if (additions.size === 0) return resolved;
235
+ if (aliasAdditions.size === 0 && synonymAdditions.size === 0) return resolved;
161
236
 
162
- // Cap expansion terms to prevent query bloat
163
- const limited = [...additions].slice(0, MAX_EXPANSION_TERMS);
237
+ // Cap expansion terms to prevent query bloat. User aliases rank first inside
238
+ // the cap — 用户为这个 trigger 显式配置的 canonical 名,不应被泛化同义词
239
+ // 挤出 MAX_EXPANSION_TERMS(P1-B:同义词"挂"曾把 alias 的实体名挤掉)。
240
+ const merged = [
241
+ ...aliasAdditions,
242
+ ...[...synonymAdditions].filter(t => !aliasAdditions.has(t)),
243
+ ];
244
+ const limited = merged.slice(0, MAX_EXPANSION_TERMS);
164
245
  return `${resolved} ${limited.join(" ")}`;
165
246
  }
@@ -12,6 +12,7 @@
12
12
 
13
13
  import type { LLMClient } from "./llm-client.js";
14
14
  import { logWarn } from "./stderr-log.js";
15
+ import * as envConfig from "./env-config.js";
15
16
 
16
17
  // ============================================================================
17
18
  // Configuration
@@ -38,7 +39,7 @@ export const DEFAULT_SYNTHESIZER_CONFIG: SynthesizerConfig = {
38
39
  * Check if synthesis is enabled via environment variable.
39
40
  */
40
41
  export function isSynthesisEnabled(): boolean {
41
- return process.env.RECALLNEST_SYNTHESIZE === "true";
42
+ return envConfig.synthesize();
42
43
  }
43
44
 
44
45
  /**
@@ -7,6 +7,7 @@
7
7
  import { createHash } from "node:crypto";
8
8
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
9
  import { join } from "node:path";
10
+ import * as envConfig from "./env-config.js";
10
11
 
11
12
  export interface RetentionPolicy {
12
13
  /** Auto-archive memories older than this many days (0 = disabled) */
@@ -29,7 +30,7 @@ function scopeHash(scope: string): string {
29
30
 
30
31
  function retentionDir(configDir?: string): string {
31
32
  return join(
32
- configDir ?? (process.env.RECALLNEST_DATA_DIR || "data"),
33
+ configDir ?? envConfig.dataDir(),
33
34
  "retention",
34
35
  );
35
36
  }