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
package/src/ingest.ts CHANGED
@@ -12,6 +12,7 @@ import { readFileSync, readdirSync, existsSync, statSync, writeFileSync } from "
12
12
  import { join, basename, resolve, dirname } from "node:path";
13
13
  import { homedir } from "node:os";
14
14
  import { detectLang, tokenizeFts } from "./language-hook.js";
15
+ import { redactSecrets } from "./pii-detector.js";
15
16
  import type { MemoryStore, MemoryEntry } from "./store.js";
16
17
  import type { Embedder } from "./embedder.js";
17
18
  import { chunkDocument, type ChunkerConfig } from "./chunker.js";
@@ -20,8 +21,10 @@ import { isConnectorOutputV1 } from "./connector-types.js";
20
21
  import { isObsidianVault, scanVault } from "./obsidian-connector.js";
21
22
  import { segmentEvents, type EventSegmenterConfig, DEFAULT_EVENT_SEGMENTER_CONFIG } from "./event-segmenter.js";
22
23
  import { isProcessed, markProcessed } from "./tracker.js";
24
+ import * as envConfig from "./env-config.js";
23
25
  import type { LLMClient, SmartExtraction } from "./llm-client.js";
24
26
  import { resolveIngestBoundary } from "./memory-boundaries.js";
27
+ import { isActiveMemory } from "./memory-evolution.js";
25
28
  import { isNoise } from "./noise-filter.js";
26
29
  import {
27
30
  inferReplyStylePreferenceSlot,
@@ -287,8 +290,12 @@ export async function dedupCheck(
287
290
  llm?: LLMClient | null,
288
291
  ): Promise<DedupCheckResult> {
289
292
  try {
290
- // Stage 1: vector similarity check
291
- const results = await store.vectorSearch(vector, DEDUP_CANDIDATE_LIMIT, DEDUP_SOFT_THRESHOLD);
293
+ // Stage 1: vector similarity check. store.vectorSearch() has no status filter, so
294
+ // superseded belief-history rows surface here — dedupe against one and incoming
295
+ // content gets dropped as a duplicate of a belief that is no longer held (returning
296
+ // to an earlier position would be silently discarded).
297
+ const results = (await store.vectorSearch(vector, DEDUP_CANDIDATE_LIMIT, DEDUP_SOFT_THRESHOLD))
298
+ .filter((result) => isActiveMemory(result.entry.metadata));
292
299
  if (results.length === 0) {
293
300
  return { action: "store", reason: "unique" }; // Clearly unique
294
301
  }
@@ -507,7 +514,7 @@ async function smartExtractBatch(
507
514
  * Tier 3.1: Check if core summary generation is enabled.
508
515
  */
509
516
  function isCoreSummaryEnabled(): boolean {
510
- return process.env.RECALLNEST_CORE_SUMMARY === "true";
517
+ return envConfig.coreSummary();
511
518
  }
512
519
 
513
520
  /**
@@ -625,7 +632,8 @@ export async function drainPendingQueue(
625
632
  let processed = 0, errors = 0;
626
633
  for (let i = 0; i < pending.length; i += 20) {
627
634
  const batch = pending.slice(i, i + 20);
628
- const texts = batch.map(c => c.text);
635
+ // F-3b: scrub before the LLM extraction call and downstream embedding
636
+ const texts = batch.map(c => redactSecrets(c.text).text);
629
637
  const extractions = await smartExtractBatch(texts, llm);
630
638
  const embeddingTexts = extractions.map(e => e.l1 || e.l0);
631
639
  const vectors = await embedder.embedBatchPassage(embeddingTexts);
@@ -860,7 +868,7 @@ function filterNoiseTurns(turns: ConversationTurn[]): ConversationTurn[] {
860
868
  return turns.filter((turn) => !isNoise(turn.text));
861
869
  }
862
870
 
863
- function groupTurnsIntoChunks(turns: ConversationTurn[]): Array<{
871
+ export function groupTurnsIntoChunks(turns: ConversationTurn[]): Array<{
864
872
  text: string;
865
873
  timestamp: string;
866
874
  sessionId: string;
@@ -933,7 +941,18 @@ function groupTurnsIntoChunks(turns: ConversationTurn[]): Array<{
933
941
  }
934
942
  }
935
943
 
936
- return chunks;
944
+ // P1 defence-in-depth: re-filter AFTER splitting. Long turns pass the
945
+ // turn-level filterNoiseTurns gate, but chunking can peel their tail into
946
+ // severed context-JSON fragments that the turn-level check never saw.
947
+ // F-3b: scrub secrets at the chunk boundary — this is the shared text
948
+ // finalization point for all session-ingest paths (CC/Codex/Gemini), before
949
+ // any chunk reaches embedding, LLM extraction, or storage.
950
+ return chunks
951
+ .filter((c) => !isNoise(c.text))
952
+ .map((c) => {
953
+ const r = redactSecrets(c.text);
954
+ return r.redacted > 0 ? { ...c, text: r.text } : c;
955
+ });
937
956
  }
938
957
 
939
958
  // ============================================================================
@@ -1027,9 +1046,14 @@ export async function ingestCodexSessions(
1027
1046
  errors: [],
1028
1047
  };
1029
1048
 
1030
- const baseDir = expandHome("~/.codex/sessions");
1031
- if (!existsSync(baseDir)) {
1032
- result.errors.push(`Codex sessions directory not found: ${baseDir}`);
1049
+ // 同时扫 sessions(活跃)+ archived_sessions(App 内归档会把文件移到这里,否则会漏)
1050
+ const baseDirs = [
1051
+ expandHome("~/.codex/sessions"),
1052
+ expandHome("~/.codex/archived_sessions"),
1053
+ ];
1054
+ const existingDirs = baseDirs.filter((d) => existsSync(d));
1055
+ if (existingDirs.length === 0) {
1056
+ result.errors.push(`Codex sessions directory not found: ${baseDirs.join(", ")}`);
1033
1057
  return result;
1034
1058
  }
1035
1059
 
@@ -1048,7 +1072,7 @@ export async function ingestCodexSessions(
1048
1072
  }
1049
1073
  }
1050
1074
  }
1051
- walk(baseDir);
1075
+ for (const dir of existingDirs) walk(dir);
1052
1076
  allFiles.sort();
1053
1077
 
1054
1078
  // --recent: only keep files modified within N hours
@@ -1171,6 +1195,276 @@ export async function ingestCodexSessions(
1171
1195
  return result;
1172
1196
  }
1173
1197
 
1198
+ // ============================================================================
1199
+ // Kimi Code Session Parser
1200
+ // ============================================================================
1201
+ // Kimi Code 的 wire.jsonl 是事件流(非 role/content 行),对话内容只在三类事件里:
1202
+ // turn.prompt (origin.kind="user") → 用户输入,.input 是 text part 数组
1203
+ // context.append_message → 消息;用户消息只收 origin.kind="user"
1204
+ // (injection/skill_activation 等是系统注入,跳过)
1205
+ // context.append_loop_event content.part → 助手文本(part.type==="text";think 跳过)
1206
+ // 其余类型(llm.tools_snapshot / mcp.tools_discovered / config.update / llm.request /
1207
+ // usage.record / permission.* 等,约占 86% 体积)是运行事件,一律跳过。
1208
+ function parseKimiSession(filePath: string): ConversationTurn[] {
1209
+ const turns: ConversationTurn[] = [];
1210
+ const content = readFileSync(filePath, "utf-8");
1211
+ const lines = content.split("\n").filter((l) => l.trim());
1212
+
1213
+ // sessionId 从路径取:.../sessions/<workDirKey>/<sessionId>/agents/main/wire.jsonl
1214
+ // 去掉 "session_" 前缀,否则 scope 统一变成 kimi:session_
1215
+ const pathParts = filePath.split("/");
1216
+ const rawId = pathParts.length >= 4 ? pathParts[pathParts.length - 4] : basename(filePath, ".jsonl");
1217
+ const sessionId = rawId.replace(/^session_/, "");
1218
+
1219
+ const pushTurn = (role: "user" | "assistant", text: string, timestamp: string) => {
1220
+ const trimmed = text.trim();
1221
+ // 中文 8 字符已是完整句;用户阈值与 codex/cc 对齐
1222
+ if (trimmed.length <= (role === "user" ? 10 : 8)) return;
1223
+ // turn.prompt 与随后的 context.append_message 重复记录同一用户输入(同 codex 相邻去重)
1224
+ const lastTurn = turns[turns.length - 1];
1225
+ if (lastTurn && lastTurn.role === role && lastTurn.text === trimmed) return;
1226
+ turns.push({ role, text: trimmed, timestamp, sessionId });
1227
+ };
1228
+
1229
+ for (const line of lines) {
1230
+ try {
1231
+ const obj = JSON.parse(line);
1232
+ const timestamp = typeof obj.time === "number" ? new Date(obj.time).toISOString() : "";
1233
+
1234
+ if (obj.type === "turn.prompt") {
1235
+ if (obj.origin?.kind !== "user") continue;
1236
+ const parts = Array.isArray(obj.input) ? obj.input : [];
1237
+ const text = parts
1238
+ .filter((c: any) => c?.type === "text")
1239
+ .map((c: any) => c.text || "")
1240
+ .join("\n");
1241
+ if (text) pushTurn("user", text, timestamp);
1242
+ continue;
1243
+ }
1244
+
1245
+ if (obj.type === "context.append_message") {
1246
+ const msg = obj.message;
1247
+ if (!msg || (msg.role !== "user" && msg.role !== "assistant")) continue;
1248
+ // 系统注入(system-reminder、skill 激活、后台任务通知)不是用户对话内容
1249
+ if (msg.role === "user" && msg.origin?.kind !== "user") continue;
1250
+ let text = "";
1251
+ if (typeof msg.content === "string") {
1252
+ text = msg.content;
1253
+ } else if (Array.isArray(msg.content)) {
1254
+ text = msg.content
1255
+ .filter((c: any) => c.type === "text")
1256
+ .map((c: any) => c.text || "")
1257
+ .join("\n");
1258
+ }
1259
+ if (text) pushTurn(msg.role, text, timestamp);
1260
+ continue;
1261
+ }
1262
+
1263
+ if (obj.type === "context.append_loop_event") {
1264
+ const part = obj.event?.type === "content.part" ? obj.event.part : null;
1265
+ if (part?.type === "text" && typeof part.text === "string") {
1266
+ pushTurn("assistant", part.text, timestamp);
1267
+ }
1268
+ }
1269
+ } catch {
1270
+ // Skip malformed lines
1271
+ }
1272
+ }
1273
+
1274
+ return turns;
1275
+ }
1276
+
1277
+ export async function ingestKimiSessions(
1278
+ store: MemoryStore,
1279
+ embedder: Embedder,
1280
+ options: { limit?: number; verbose?: boolean; noDedup?: boolean; llm?: LLMClient | null; recentHours?: number } = {},
1281
+ ): Promise<IngestResult> {
1282
+ const result: IngestResult = {
1283
+ source: "kimi",
1284
+ filesProcessed: 0,
1285
+ chunksIngested: 0,
1286
+ chunksSkipped: 0,
1287
+ chunksDeduped: 0,
1288
+ dedupReasonCounts: createDedupReasonCounts(),
1289
+ errors: [],
1290
+ };
1291
+
1292
+ const baseDir = expandHome("~/.kimi-code/sessions");
1293
+ const indexPath = expandHome("~/.kimi-code/session_index.jsonl");
1294
+
1295
+ // 文件发现:优先 session_index.jsonl(精确),再 walk sessions 目录兜底(索引缺漏时)。
1296
+ // 只收 agents/main/wire.jsonl,子 agent 的 agents/agent-N/wire.jsonl 与主对话重复,不采。
1297
+ const seen = new Set<string>();
1298
+ const allFiles: string[] = [];
1299
+ const addFile = (p: string) => {
1300
+ if (!seen.has(p) && existsSync(p)) {
1301
+ seen.add(p);
1302
+ allFiles.push(p);
1303
+ }
1304
+ };
1305
+
1306
+ if (existsSync(indexPath)) {
1307
+ try {
1308
+ for (const line of readFileSync(indexPath, "utf-8").split("\n")) {
1309
+ if (!line.trim()) continue;
1310
+ try {
1311
+ const entry = JSON.parse(line);
1312
+ if (typeof entry.sessionDir === "string" && entry.sessionDir) {
1313
+ addFile(join(entry.sessionDir, "agents", "main", "wire.jsonl"));
1314
+ }
1315
+ } catch {
1316
+ // Skip malformed index lines
1317
+ }
1318
+ }
1319
+ } catch {
1320
+ // 索引读不出来就走下面的目录扫描
1321
+ }
1322
+ }
1323
+
1324
+ if (existsSync(baseDir)) {
1325
+ const mainWireSuffix = join("agents", "main", "wire.jsonl");
1326
+ const walk = (dir: string) => {
1327
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
1328
+ const full = join(dir, entry.name);
1329
+ if (entry.isDirectory()) {
1330
+ walk(full);
1331
+ } else if (full.endsWith(mainWireSuffix)) {
1332
+ addFile(full);
1333
+ }
1334
+ }
1335
+ };
1336
+ walk(baseDir);
1337
+ }
1338
+
1339
+ if (allFiles.length === 0) {
1340
+ result.errors.push(`Kimi Code sessions not found: ${baseDir} (index: ${indexPath})`);
1341
+ return result;
1342
+ }
1343
+ allFiles.sort();
1344
+
1345
+ const recentMode = options.recentHours !== undefined;
1346
+ const cutoffMs = recentMode ? Date.now() - options.recentHours! * 3600_000 : 0;
1347
+
1348
+ // --recent: only keep files modified within N hours
1349
+ const filteredFiles = recentMode
1350
+ ? allFiles.filter((f) => { try { return statSync(f).mtimeMs >= cutoffMs; } catch { return false; } })
1351
+ : allFiles;
1352
+
1353
+ const filesToProcess = options.limit ? filteredFiles.slice(0, options.limit) : filteredFiles;
1354
+ const total = filesToProcess.length;
1355
+
1356
+ for (let fi = 0; fi < filesToProcess.length; fi++) {
1357
+ const filePath = filesToProcess[fi];
1358
+
1359
+ try {
1360
+ const stat = statSync(filePath);
1361
+ if (stat.size < 200) {
1362
+ result.chunksSkipped++;
1363
+ continue;
1364
+ }
1365
+
1366
+ // In --recent mode, skip ingested-files check — files may have new content appended
1367
+ if (!recentMode && isProcessed(filePath, stat.size, stat.mtimeMs)) {
1368
+ result.chunksSkipped++;
1369
+ continue;
1370
+ }
1371
+
1372
+ const turns = parseKimiSession(filePath);
1373
+ if (turns.length === 0) {
1374
+ result.chunksSkipped++;
1375
+ markProcessed(filePath, stat.size, 0, stat.mtimeMs);
1376
+ continue;
1377
+ }
1378
+
1379
+ const chunks = groupTurnsIntoChunks(turns);
1380
+ const texts = chunks.map((c) => c.text);
1381
+ const batchSize = 32;
1382
+ let fileChunks = 0;
1383
+
1384
+ for (let i = 0; i < texts.length; i += batchSize) {
1385
+ const batch = texts.slice(i, i + batchSize);
1386
+ const batchChunks = chunks.slice(i, i + batchSize);
1387
+
1388
+ try {
1389
+ const vectors = await embedder.embedBatchPassage(batch);
1390
+ const toStore: Array<{text: string; vector: number[]; category: string; scope: string; importance: number; metadata: string}> = [];
1391
+
1392
+ // Dedup + L0 batch
1393
+ const dedupedTexts: string[] = [];
1394
+ const dedupedVectors: number[][] = [];
1395
+ const dedupedChunks: typeof batchChunks = [];
1396
+
1397
+ for (let j = 0; j < batch.length; j++) {
1398
+ const vector = vectors[j];
1399
+ if (!vector || vector.length === 0) {
1400
+ result.chunksSkipped++;
1401
+ continue;
1402
+ }
1403
+ if (!options.noDedup) {
1404
+ const decision = await dedupCheck(store, vector, batch[j], options.llm);
1405
+ recordDedupDecision(result, decision);
1406
+ if (decision.secondaryDeletes?.length) {
1407
+ await executeSecondaryDeletes(store, decision.secondaryDeletes);
1408
+ }
1409
+ if (decision.action === "skip") continue;
1410
+ }
1411
+ dedupedTexts.push(batch[j]);
1412
+ dedupedVectors.push(vector);
1413
+ dedupedChunks.push(batchChunks[j]);
1414
+ }
1415
+
1416
+ if (dedupedTexts.length > 0) {
1417
+ if (!options.llm) {
1418
+ queueForLaterExtraction(
1419
+ dedupedChunks.map(c => ({ text: c.text, scope: `kimi:${c.sessionId.slice(0, 8)}` }))
1420
+ );
1421
+ result.chunksSkipped += dedupedTexts.length;
1422
+ } else {
1423
+ const extractions = await smartExtractBatch(dedupedTexts, options.llm);
1424
+ const coreSummaries = await generateCoreSummaries(dedupedTexts, options.llm);
1425
+ for (let j = 0; j < dedupedTexts.length; j++) {
1426
+ const chunk = dedupedChunks[j];
1427
+ const ext = extractions[j];
1428
+ toStore.push(buildIngestedEntry({
1429
+ source: "kimi",
1430
+ scope: `kimi:${chunk.sessionId.slice(0, 8)}`,
1431
+ text: dedupedTexts[j],
1432
+ vector: dedupedVectors[j],
1433
+ extraction: ext,
1434
+ sessionId: chunk.sessionId,
1435
+ file: basename(filePath),
1436
+ coreSummary: coreSummaries[j],
1437
+ }));
1438
+ }
1439
+ }
1440
+ }
1441
+
1442
+ if (toStore.length > 0) {
1443
+ await store.storeBatch(toStore);
1444
+ result.chunksIngested += toStore.length;
1445
+ fileChunks += toStore.length;
1446
+ }
1447
+ } catch (err: any) {
1448
+ result.errors.push(`Embedding batch error: ${err.message}`);
1449
+ }
1450
+ }
1451
+
1452
+ markProcessed(filePath, stat.size, fileChunks, stat.mtimeMs);
1453
+ result.filesProcessed++;
1454
+
1455
+ if ((fi + 1) % 10 === 0 || fi + 1 === total) {
1456
+ console.log(
1457
+ ` Kimi: ${fi + 1}/${total} files, ${result.chunksIngested} chunks ingested`,
1458
+ );
1459
+ }
1460
+ } catch (err: any) {
1461
+ result.errors.push(`Error processing ${basename(filePath)}: ${err.message}`);
1462
+ }
1463
+ }
1464
+
1465
+ return result;
1466
+ }
1467
+
1174
1468
  // ============================================================================
1175
1469
  // Gemini Session Parser
1176
1470
  // ============================================================================
@@ -1650,7 +1944,8 @@ export async function ingestMarkdownFiles(
1650
1944
  continue;
1651
1945
  }
1652
1946
 
1653
- const texts = sections.map((s) => s.text);
1947
+ // F-3b: scrub before embedding and storage
1948
+ const texts = sections.map((s) => redactSecrets(s.text).text);
1654
1949
  const batchSize = 32;
1655
1950
  let fileChunks = 0;
1656
1951
 
@@ -1788,7 +2083,8 @@ export async function ingestGenericText(
1788
2083
 
1789
2084
  const chunkResult = chunkDocument(textToChunk, CONVERSATION_CHUNK_CONFIG);
1790
2085
 
1791
- const texts = chunkResult.chunks;
2086
+ // F-3b: scrub before embedding and storage
2087
+ const texts = chunkResult.chunks.map((t) => redactSecrets(t).text);
1792
2088
  const batchSize = 32;
1793
2089
 
1794
2090
  for (let i = 0; i < texts.length; i += batchSize) {
@@ -8,6 +8,7 @@ import type { LLMClient } from "./llm-client.js";
8
8
  import type { KGStore } from "./kg-store.js";
9
9
  import { logInfo, logWarn } from "./stderr-log.js";
10
10
  import { detectLang, getKgPromptHook } from "./language-hook.js";
11
+ import * as envConfig from "./env-config.js";
11
12
 
12
13
  // ============================================================================
13
14
  // Types
@@ -39,7 +40,7 @@ interface LlmExtractionResponse {
39
40
 
40
41
  /** Check if KG mode is enabled (env var) */
41
42
  export function isKGModeEnabled(): boolean {
42
- return process.env.RECALLNEST_KG_MODE === "true";
43
+ return envConfig.kgMode();
43
44
  }
44
45
 
45
46
  // ============================================================================
@@ -95,7 +96,13 @@ const DEFAULT_KG_SYSTEM = `You are a knowledge graph extraction assistant. Extra
95
96
  const DEFAULT_KG_USER_TEMPLATE = `Extract knowledge graph triples from the following text.
96
97
  Rules:
97
98
  - Use simple predicates: uses, created_by, works_with, is_a, part_of, has, located_in, belongs_to, depends_on, implements, extends, related_to, caused_by, results_in, precedes, follows
98
- - Assign confidence 0.0-1.0
99
+ - confidence must be EXACTLY one of these five values (models collapse continuous ranges — pick the one matching the evidence):
100
+ 1.0 = stated explicitly in the text
101
+ 0.85 = strong inference (clearly implied, never stated)
102
+ 0.75 = reasonable inference (requires interpretation)
103
+ 0.65 = weak inference (thematic association only)
104
+ 0.55 = speculative (surface co-occurrence)
105
+ Never use 0.5 or any value outside this list. If unsure between two tiers, pick the lower.
99
106
  - Extract 3-8 triples
100
107
  - Return JSON: { "triples": [{ "subject": "", "predicate": "", "object": "", "confidence": 0.0 }] }
101
108
 
@@ -163,8 +170,11 @@ export class KGExtractor {
163
170
  prompt,
164
171
  );
165
172
  } catch (err) {
173
+ // Transport/API failure is NOT "zero triples" — propagate so callers can
174
+ // distinguish (backfill must not journal it as processed). The capture
175
+ // path's fire-and-forget .catch() keeps memory writes unaffected.
166
176
  logWarn(`[KG] LLM extraction failed: ${String(err)}`);
167
- return [];
177
+ throw err instanceof Error ? err : new Error(String(err));
168
178
  }
169
179
 
170
180
  if (!response?.triples || !Array.isArray(response.triples)) {