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
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Recall gate — pre-retrieval triage for auto-recall traffic.
3
+ *
4
+ * Borrowed from memory-lancedb-pro's adaptive-retrieval, reshaped per Codex
5
+ * review (2026-07-17) into a three-way verdict instead of a binary SKIP/FORCE:
6
+ *
7
+ * - "full-recall": message explicitly reaches for memory ("记得/上次/my
8
+ * preference") — always run resume + focused search.
9
+ * - "resume-only": bare continuity nudges ("继续", "下一步", "开始吧") —
10
+ * they need checkpoint/continuity, not an embedding search
11
+ * over their own two characters.
12
+ * - "skip-all": greetings, slash commands, heartbeats, whole-message
13
+ * acks, pure emoji, bare CLI invocations — no memory value.
14
+ * - "pass": no rule matched — default full pipeline.
15
+ *
16
+ * Rollout is governed by RECALLNEST_RECALL_GATE (observe-before-enforce,
17
+ * shared-behaviors §5): "observe" (default) computes the verdict and appends a
18
+ * shadow-log line but never changes behavior; "enforce" acts on the verdict;
19
+ * "off" disables even the shadow logging. Shadow entries carry rule id,
20
+ * message length, and decision — never the message text itself.
21
+ */
22
+
23
+ import { appendFileSync, existsSync, mkdirSync } from "node:fs";
24
+ import { join } from "node:path";
25
+ import { dataDir } from "./env-config.js";
26
+
27
+ export type RecallGateDecision = "full-recall" | "resume-only" | "skip-all" | "pass";
28
+
29
+ export interface RecallGateResult {
30
+ decision: RecallGateDecision;
31
+ /** Stable rule identifier, e.g. "force:memory-cue", "skip:ack", "pass:long". */
32
+ ruleId: string;
33
+ }
34
+
35
+ export type RecallGateMode = "observe" | "enforce" | "off";
36
+
37
+ /** RECALLNEST_RECALL_GATE: unset/invalid → "observe" (shadow-only default). */
38
+ export function resolveRecallGateMode(env: NodeJS.ProcessEnv = process.env): RecallGateMode {
39
+ const raw = (env.RECALLNEST_RECALL_GATE || "").trim().toLowerCase();
40
+ if (raw === "enforce") return "enforce";
41
+ if (raw === "off") return "off";
42
+ return "observe";
43
+ }
44
+
45
+ // Short-text rules below only ever fire on messages up to this length; longer
46
+ // messages carry enough content to always deserve the full pipeline.
47
+ const SHORT_TEXT_MAX_CHARS = 80;
48
+
49
+ // --- full-recall cues (substring, any position, force-first per review) ---
50
+
51
+ const FORCE_CUES_ZH = [
52
+ "记得",
53
+ "回忆",
54
+ "想起",
55
+ "上次",
56
+ "上回",
57
+ "之前讨论",
58
+ "之前说",
59
+ "之前聊",
60
+ "之前提",
61
+ "我的偏好",
62
+ "我说过",
63
+ "你说过",
64
+ "说过吗",
65
+ "存过",
66
+ "记过",
67
+ "提醒过",
68
+ ];
69
+
70
+ // Word-bounded and hyphen/underscore-guarded: "recall" must not fire on
71
+ // "auto-recall" or "recall-gate.test.ts" (identifiers, not memory intent).
72
+ const FORCE_CUES_EN = [
73
+ /(?<![\w-])remember(?![\w-])/,
74
+ /(?<![\w-])recall(?![\w-])/,
75
+ /(?<![\w-])last time(?![\w-])/,
76
+ /(?<![\w-])previously(?![\w-])/,
77
+ /(?<![\w-])we discussed(?![\w-])/,
78
+ /(?<![\w-])my preferences?(?![\w-])/,
79
+ /(?<![\w-])you said(?![\w-])/,
80
+ /(?<![\w-])i told you(?![\w-])/,
81
+ ];
82
+
83
+ // --- resume-only: whole-message continuity nudges ---
84
+
85
+ // Matched against the whole message after trailing particles/punctuation are
86
+ // stripped, so "继续吧" hits but "继续讨论刚才的方案" (real content) passes.
87
+ const RESUME_PHRASES = new Set([
88
+ "继续",
89
+ "接着",
90
+ "接着来",
91
+ "接着做",
92
+ "接着干",
93
+ "接着说",
94
+ "继续做",
95
+ "继续干",
96
+ "下一步",
97
+ "开始",
98
+ "开工",
99
+ "走起",
100
+ "往下",
101
+ "往下走",
102
+ "continue",
103
+ "go on",
104
+ "next",
105
+ "next step",
106
+ "resume",
107
+ "keep going",
108
+ "carry on",
109
+ "proceed",
110
+ "pick up where we left off",
111
+ ]);
112
+
113
+ // --- skip-all: whole-message greetings & acks ---
114
+
115
+ const GREETINGS = new Set([
116
+ "你好",
117
+ "您好",
118
+ "哈喽",
119
+ "嗨",
120
+ "早",
121
+ "早上好",
122
+ "早安",
123
+ "中午好",
124
+ "下午好",
125
+ "晚上好",
126
+ "晚安",
127
+ "在吗",
128
+ "在不在",
129
+ "在么",
130
+ "hi",
131
+ "hello",
132
+ "hey",
133
+ "yo",
134
+ "good morning",
135
+ "good night",
136
+ "good evening",
137
+ "morning",
138
+ "gm",
139
+ "gn",
140
+ ]);
141
+
142
+ const ACK_WORDS = new Set([
143
+ "好的",
144
+ "好",
145
+ "好嘞",
146
+ "好滴",
147
+ "好呀",
148
+ "嗯",
149
+ "嗯嗯",
150
+ "哦",
151
+ "噢",
152
+ "喔",
153
+ "行",
154
+ "可以",
155
+ "可",
156
+ "没问题",
157
+ "没事",
158
+ "收到",
159
+ "明白",
160
+ "了解",
161
+ "知道了",
162
+ "懂了",
163
+ "对",
164
+ "对的",
165
+ "是",
166
+ "是的",
167
+ "不用",
168
+ "不用了",
169
+ "谢谢",
170
+ "谢了",
171
+ "多谢",
172
+ "辛苦了",
173
+ "辛苦",
174
+ "牛",
175
+ "赞",
176
+ "棒",
177
+ "妙",
178
+ "ok",
179
+ "okay",
180
+ "k",
181
+ "kk",
182
+ "yes",
183
+ "yep",
184
+ "yeah",
185
+ "no",
186
+ "nope",
187
+ "sure",
188
+ "thanks",
189
+ "thank you",
190
+ "thx",
191
+ "ty",
192
+ "cool",
193
+ "nice",
194
+ "great",
195
+ "done",
196
+ "got it",
197
+ "will do",
198
+ ]);
199
+
200
+ // Bare CLI invocations ("git push", "npm test"). A leading tool name alone is
201
+ // not enough — the rest must look like arguments, not prose: any CJK character
202
+ // or question mark ("git 为什么失败", "why does npm test hang?") disqualifies.
203
+ const CLI_SHAPE =
204
+ /^(git|npm|npx|bun|bunx|yarn|pnpm|node|python3?|pip3?|cargo|make|docker|kubectl|brew|ssh|ls|cd|cat|grep|curl)(\s+[^㐀-鿿??]*)?$/i;
205
+
206
+ // Emoji message test: strip whitespace, then require every remaining unit to
207
+ // be pictographic or an emoji modifier. \p{Extended_Pictographic} deliberately
208
+ // excludes bare digits/#/* (they only render as emoji inside keycap sequences
209
+ // with U+FE0F+U+20E3), so "42" and "#1" fall through to "pass".
210
+ const EMOJI_UNIT =
211
+ /^(?:\p{Extended_Pictographic}|\p{Regional_Indicator}|[\u{1F3FB}-\u{1F3FF}\u{FE0F}\u{200D}\u{20E3}])+$/u;
212
+
213
+ // Trailing tone particles + punctuation tolerated on whole-message matches
214
+ // ("好的吧。", "继续吧!", "开始咯~").
215
+ const TRAILING_FLUFF = /[吧呗啊呀哦嘛喽咯哈嘞捏诶欸了]*[\s。,,.!!??~〜…‥·、;;::'"'"()()]*$/u;
216
+
217
+ function stripTrailingFluff(text: string): string {
218
+ return text.replace(TRAILING_FLUFF, "");
219
+ }
220
+
221
+ function normalize(message: string): string {
222
+ return message.replace(/\s+/g, " ").trim();
223
+ }
224
+
225
+ function isAckMessage(lower: string): boolean {
226
+ const stripped = stripTrailingFluff(lower);
227
+ if (!stripped) return false;
228
+ if (ACK_WORDS.has(stripped)) return true;
229
+ // Multi-token acks ("好的 谢谢", "ok thanks"): every token must be an ack word.
230
+ const tokens = stripped.split(/[\s,,。!!~]+/).filter(Boolean);
231
+ return tokens.length > 1 && tokens.every((t) => ACK_WORDS.has(stripTrailingFluff(t)));
232
+ }
233
+
234
+ /**
235
+ * Classify one auto-recall message. Pure and deterministic — call order is
236
+ * force cues → length guard → structural skips → whole-message skips →
237
+ * resume nudges → default pass.
238
+ */
239
+ export function classifyRecallGate(message: string): RecallGateResult {
240
+ const text = normalize(message);
241
+ if (!text) return { decision: "skip-all", ruleId: "skip:empty" };
242
+ const lower = text.toLowerCase();
243
+
244
+ // 1. Force cues outrank every short-text rule ("可以按上次方案做" must not
245
+ // be swallowed by the ack rule).
246
+ for (const cue of FORCE_CUES_ZH) {
247
+ if (text.includes(cue)) return { decision: "full-recall", ruleId: "force:memory-cue-zh" };
248
+ }
249
+ for (const cue of FORCE_CUES_EN) {
250
+ if (cue.test(lower)) return { decision: "full-recall", ruleId: "force:memory-cue-en" };
251
+ }
252
+
253
+ // 2. Long messages always deserve the full pipeline.
254
+ if (text.length > SHORT_TEXT_MAX_CHARS) return { decision: "pass", ruleId: "pass:long" };
255
+
256
+ // 3. Structural skips.
257
+ if (/^\/\S+/.test(text)) return { decision: "skip-all", ruleId: "skip:slash" };
258
+ if (/^HEARTBEAT/i.test(text)) return { decision: "skip-all", ruleId: "skip:heartbeat" };
259
+ if (EMOJI_UNIT.test(text.replace(/\s+/g, ""))) {
260
+ return { decision: "skip-all", ruleId: "skip:emoji" };
261
+ }
262
+ if (text.length <= 60 && CLI_SHAPE.test(text)) {
263
+ return { decision: "skip-all", ruleId: "skip:cli" };
264
+ }
265
+
266
+ // 4. Whole-message conversational skips.
267
+ const strippedLower = stripTrailingFluff(lower);
268
+ if (GREETINGS.has(strippedLower)) return { decision: "skip-all", ruleId: "skip:greeting" };
269
+ if (isAckMessage(lower)) return { decision: "skip-all", ruleId: "skip:ack" };
270
+
271
+ // 5. Continuity nudges → resume context without a focused search.
272
+ if (RESUME_PHRASES.has(strippedLower)) {
273
+ return { decision: "resume-only", ruleId: "resume:continuity" };
274
+ }
275
+
276
+ return { decision: "pass", ruleId: "pass:default" };
277
+ }
278
+
279
+ // --- shadow log (observe-before-enforce evidence trail) ---
280
+
281
+ export interface RecallGateShadowEntry {
282
+ ts: string;
283
+ decision: RecallGateDecision;
284
+ ruleId: string;
285
+ msgLen: number;
286
+ mode: Exclude<RecallGateMode, "off">;
287
+ /** Caller-provided origin, e.g. "api:/v1/auto-recall". Never message text. */
288
+ source?: string;
289
+ }
290
+
291
+ export const RECALL_GATE_SHADOW_FILE = "recall-gate-shadow.jsonl";
292
+
293
+ /**
294
+ * Append one shadow-log line under dataDir(). Must never block or throw —
295
+ * gate telemetry failing is strictly better than recall failing.
296
+ */
297
+ export function logRecallGateShadow(entry: RecallGateShadowEntry): void {
298
+ try {
299
+ const dir = dataDir();
300
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
301
+ appendFileSync(join(dir, RECALL_GATE_SHADOW_FILE), `${JSON.stringify(entry)}\n`, "utf8");
302
+ } catch {
303
+ // Shadow logging is best-effort by design.
304
+ }
305
+ }
@@ -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
  }
@@ -1,7 +1,46 @@
1
- import type { RetrievalConfig } from "./retriever.js";
1
+ import {
2
+ DEFAULT_CATEGORY_MIN_SCORES,
3
+ DEFAULT_RETRIEVAL_CONFIG,
4
+ type RetrievalConfig,
5
+ } from "./retriever.js";
2
6
 
3
7
  export type RetrievalProfileName = "default" | "writing" | "debug" | "fact-check";
4
8
 
9
+ /**
10
+ * Scale the default per-category thresholds by how loose/tight a profile is overall.
11
+ *
12
+ * Why this exists: a profile used to override `hardMinScore` only, while
13
+ * `Retriever.minScoreFor()` reads `categoryMinScores` first and only falls back to
14
+ * `hardMinScore` for categories not listed. Since the default map lists every
15
+ * category, a profile's looser/tighter intent never reached category admission at
16
+ * all — picking `debug` ("I want episodic detail") still admitted `events` at 0.35
17
+ * and `patterns` at 0.45, exactly as `default` would.
18
+ *
19
+ * IMPORTANT: the returned map must stay complete. A partial map would silently send
20
+ * unlisted categories to `hardMinScore` instead of their default threshold.
21
+ *
22
+ * simplified: proportional scaling only — the *relative* ordering across categories
23
+ * is inherited from DEFAULT_CATEGORY_MIN_SCORES unchanged. Re-designing that ordering
24
+ * per profile (e.g. `debug` favouring episodic categories, `writing` favouring
25
+ * semantic ones) is the better end state but needs canary evidence to pick numbers;
26
+ * do that via `bun run eval:canary` before touching the ratios here.
27
+ */
28
+ function scaledCategoryMinScores(profileHardMinScore: number): Record<string, number> {
29
+ const factor = profileHardMinScore / DEFAULT_RETRIEVAL_CONFIG.hardMinScore;
30
+ return Object.fromEntries(
31
+ Object.entries(DEFAULT_CATEGORY_MIN_SCORES).map(([category, minScore]) => [
32
+ category,
33
+ Math.round(minScore * factor * 1000) / 1000,
34
+ ]),
35
+ );
36
+ }
37
+
38
+ // Kept as named constants so each profile's `hardMinScore` and its scaled category
39
+ // thresholds cannot drift apart.
40
+ const WRITING_HARD_MIN_SCORE = 0.24;
41
+ const DEBUG_HARD_MIN_SCORE = 0.34;
42
+ const FACT_CHECK_HARD_MIN_SCORE = 0.38;
43
+
5
44
  export interface RetrievalProfileDefinition {
6
45
  name: RetrievalProfileName;
7
46
  label: string;
@@ -27,7 +66,8 @@ export const RETRIEVAL_PROFILES: Record<RetrievalProfileName, RetrievalProfileDe
27
66
  recencyHalfLifeDays: 90,
28
67
  recencyWeight: 0.06,
29
68
  timeDecayHalfLifeDays: 365,
30
- hardMinScore: 0.24,
69
+ hardMinScore: WRITING_HARD_MIN_SCORE,
70
+ categoryMinScores: scaledCategoryMinScores(WRITING_HARD_MIN_SCORE),
31
71
  lengthNormAnchor: 800,
32
72
  },
33
73
  },
@@ -42,7 +82,8 @@ export const RETRIEVAL_PROFILES: Record<RetrievalProfileName, RetrievalProfileDe
42
82
  recencyHalfLifeDays: 7,
43
83
  recencyWeight: 0.15,
44
84
  timeDecayHalfLifeDays: 30,
45
- hardMinScore: 0.34,
85
+ hardMinScore: DEBUG_HARD_MIN_SCORE,
86
+ categoryMinScores: scaledCategoryMinScores(DEBUG_HARD_MIN_SCORE),
46
87
  lengthNormAnchor: 420,
47
88
  },
48
89
  },
@@ -57,7 +98,8 @@ export const RETRIEVAL_PROFILES: Record<RetrievalProfileName, RetrievalProfileDe
57
98
  recencyHalfLifeDays: 21,
58
99
  recencyWeight: 0.08,
59
100
  timeDecayHalfLifeDays: 45,
60
- hardMinScore: 0.38,
101
+ hardMinScore: FACT_CHECK_HARD_MIN_SCORE,
102
+ categoryMinScores: scaledCategoryMinScores(FACT_CHECK_HARD_MIN_SCORE),
61
103
  lengthNormAnchor: 360,
62
104
  },
63
105
  },