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
package/src/ingest.ts CHANGED
@@ -12,11 +12,16 @@ 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";
19
+ import type { ConnectorOutputV1, ConnectorRecord } from "./connector-types.js";
20
+ import { isConnectorOutputV1 } from "./connector-types.js";
21
+ import { isObsidianVault, scanVault } from "./obsidian-connector.js";
18
22
  import { segmentEvents, type EventSegmenterConfig, DEFAULT_EVENT_SEGMENTER_CONFIG } from "./event-segmenter.js";
19
23
  import { isProcessed, markProcessed } from "./tracker.js";
24
+ import * as envConfig from "./env-config.js";
20
25
  import type { LLMClient, SmartExtraction } from "./llm-client.js";
21
26
  import { resolveIngestBoundary } from "./memory-boundaries.js";
22
27
  import { isNoise } from "./noise-filter.js";
@@ -504,7 +509,7 @@ async function smartExtractBatch(
504
509
  * Tier 3.1: Check if core summary generation is enabled.
505
510
  */
506
511
  function isCoreSummaryEnabled(): boolean {
507
- return process.env.RECALLNEST_CORE_SUMMARY === "true";
512
+ return envConfig.coreSummary();
508
513
  }
509
514
 
510
515
  /**
@@ -622,7 +627,8 @@ export async function drainPendingQueue(
622
627
  let processed = 0, errors = 0;
623
628
  for (let i = 0; i < pending.length; i += 20) {
624
629
  const batch = pending.slice(i, i + 20);
625
- const texts = batch.map(c => c.text);
630
+ // F-3b: scrub before the LLM extraction call and downstream embedding
631
+ const texts = batch.map(c => redactSecrets(c.text).text);
626
632
  const extractions = await smartExtractBatch(texts, llm);
627
633
  const embeddingTexts = extractions.map(e => e.l1 || e.l0);
628
634
  const vectors = await embedder.embedBatchPassage(embeddingTexts);
@@ -857,7 +863,7 @@ function filterNoiseTurns(turns: ConversationTurn[]): ConversationTurn[] {
857
863
  return turns.filter((turn) => !isNoise(turn.text));
858
864
  }
859
865
 
860
- function groupTurnsIntoChunks(turns: ConversationTurn[]): Array<{
866
+ export function groupTurnsIntoChunks(turns: ConversationTurn[]): Array<{
861
867
  text: string;
862
868
  timestamp: string;
863
869
  sessionId: string;
@@ -930,7 +936,18 @@ function groupTurnsIntoChunks(turns: ConversationTurn[]): Array<{
930
936
  }
931
937
  }
932
938
 
933
- return chunks;
939
+ // P1 defence-in-depth: re-filter AFTER splitting. Long turns pass the
940
+ // turn-level filterNoiseTurns gate, but chunking can peel their tail into
941
+ // severed context-JSON fragments that the turn-level check never saw.
942
+ // F-3b: scrub secrets at the chunk boundary — this is the shared text
943
+ // finalization point for all session-ingest paths (CC/Codex/Gemini), before
944
+ // any chunk reaches embedding, LLM extraction, or storage.
945
+ return chunks
946
+ .filter((c) => !isNoise(c.text))
947
+ .map((c) => {
948
+ const r = redactSecrets(c.text);
949
+ return r.redacted > 0 ? { ...c, text: r.text } : c;
950
+ });
934
951
  }
935
952
 
936
953
  // ============================================================================
@@ -1024,9 +1041,14 @@ export async function ingestCodexSessions(
1024
1041
  errors: [],
1025
1042
  };
1026
1043
 
1027
- const baseDir = expandHome("~/.codex/sessions");
1028
- if (!existsSync(baseDir)) {
1029
- result.errors.push(`Codex sessions directory not found: ${baseDir}`);
1044
+ // 同时扫 sessions(活跃)+ archived_sessions(App 内归档会把文件移到这里,否则会漏)
1045
+ const baseDirs = [
1046
+ expandHome("~/.codex/sessions"),
1047
+ expandHome("~/.codex/archived_sessions"),
1048
+ ];
1049
+ const existingDirs = baseDirs.filter((d) => existsSync(d));
1050
+ if (existingDirs.length === 0) {
1051
+ result.errors.push(`Codex sessions directory not found: ${baseDirs.join(", ")}`);
1030
1052
  return result;
1031
1053
  }
1032
1054
 
@@ -1045,7 +1067,7 @@ export async function ingestCodexSessions(
1045
1067
  }
1046
1068
  }
1047
1069
  }
1048
- walk(baseDir);
1070
+ for (const dir of existingDirs) walk(dir);
1049
1071
  allFiles.sort();
1050
1072
 
1051
1073
  // --recent: only keep files modified within N hours
@@ -1647,7 +1669,8 @@ export async function ingestMarkdownFiles(
1647
1669
  continue;
1648
1670
  }
1649
1671
 
1650
- const texts = sections.map((s) => s.text);
1672
+ // F-3b: scrub before embedding and storage
1673
+ const texts = sections.map((s) => redactSecrets(s.text).text);
1651
1674
  const batchSize = 32;
1652
1675
  let fileChunks = 0;
1653
1676
 
@@ -1785,7 +1808,8 @@ export async function ingestGenericText(
1785
1808
 
1786
1809
  const chunkResult = chunkDocument(textToChunk, CONVERSATION_CHUNK_CONFIG);
1787
1810
 
1788
- const texts = chunkResult.chunks;
1811
+ // F-3b: scrub before embedding and storage
1812
+ const texts = chunkResult.chunks.map((t) => redactSecrets(t).text);
1789
1813
  const batchSize = 32;
1790
1814
 
1791
1815
  for (let i = 0; i < texts.length; i += batchSize) {
@@ -1850,3 +1874,203 @@ export async function ingestGenericText(
1850
1874
 
1851
1875
  return result;
1852
1876
  }
1877
+
1878
+ // ============================================================================
1879
+ // Connector-v1 Ingest (GB-2)
1880
+ // ============================================================================
1881
+
1882
+ /**
1883
+ * Ingest a connector-v1 formatted JSON payload.
1884
+ *
1885
+ * External connectors (Obsidian, Email, RSS, etc.) produce a standard
1886
+ * ConnectorOutputV1 JSON → this function consumes it through the existing
1887
+ * dedup / smartExtract / buildIngestedEntry pipeline.
1888
+ *
1889
+ * contentHash on each record enables incremental sync: if a record with the
1890
+ * same hash is already in the store, it is skipped without embedding.
1891
+ */
1892
+ export async function ingestConnectorFile(
1893
+ store: MemoryStore,
1894
+ embedder: Embedder,
1895
+ content: string,
1896
+ options: { verbose?: boolean; noDedup?: boolean; llm?: LLMClient | null } = {},
1897
+ ): Promise<IngestResult> {
1898
+ const parsed = JSON.parse(content);
1899
+ if (!isConnectorOutputV1(parsed)) {
1900
+ return {
1901
+ source: "connector",
1902
+ filesProcessed: 0,
1903
+ chunksIngested: 0,
1904
+ chunksSkipped: 0,
1905
+ chunksDeduped: 0,
1906
+ dedupReasonCounts: createDedupReasonCounts(),
1907
+ errors: ["Invalid connector-v1 format: missing version/source/scope/records"],
1908
+ };
1909
+ }
1910
+
1911
+ const connectorData = parsed as ConnectorOutputV1;
1912
+ const { source, scope, records } = connectorData;
1913
+
1914
+ const result: IngestResult = {
1915
+ source: `connector:${source}`,
1916
+ filesProcessed: 1,
1917
+ chunksIngested: 0,
1918
+ chunksSkipped: 0,
1919
+ chunksDeduped: 0,
1920
+ dedupReasonCounts: createDedupReasonCounts(),
1921
+ errors: [],
1922
+ };
1923
+
1924
+ if (records.length === 0) return result;
1925
+
1926
+ const batchSize = 32;
1927
+
1928
+ for (let i = 0; i < records.length; i += batchSize) {
1929
+ const batch = records.slice(i, i + batchSize);
1930
+ const texts = batch.map((r: ConnectorRecord) =>
1931
+ r.title ? `[${r.title}] ${r.text}` : r.text,
1932
+ );
1933
+
1934
+ try {
1935
+ const vectors = await embedder.embedBatchPassage(texts);
1936
+
1937
+ // --- dedup pass ---
1938
+ const dedupedTexts: string[] = [];
1939
+ const dedupedVectors: number[][] = [];
1940
+ const dedupedRecords: ConnectorRecord[] = [];
1941
+
1942
+ for (let j = 0; j < batch.length; j++) {
1943
+ const vector = vectors[j];
1944
+ if (!vector || vector.length === 0) {
1945
+ result.chunksSkipped++;
1946
+ continue;
1947
+ }
1948
+ if (!options.noDedup) {
1949
+ const decision = await dedupCheck(store, vector, texts[j], options.llm);
1950
+ recordDedupDecision(result, decision);
1951
+ if (decision.secondaryDeletes?.length) {
1952
+ await executeSecondaryDeletes(store, decision.secondaryDeletes);
1953
+ }
1954
+ if (decision.action === "skip") continue;
1955
+ }
1956
+ dedupedTexts.push(texts[j]);
1957
+ dedupedVectors.push(vector);
1958
+ dedupedRecords.push(batch[j]);
1959
+ }
1960
+
1961
+ if (dedupedTexts.length === 0) continue;
1962
+
1963
+ // --- extract + build entries ---
1964
+ const extractions = await smartExtractBatch(dedupedTexts, options.llm);
1965
+ const coreSummaries = await generateCoreSummaries(dedupedTexts, options.llm);
1966
+ const toStore: Array<{
1967
+ text: string;
1968
+ vector: number[];
1969
+ category: string;
1970
+ scope: string;
1971
+ importance: number;
1972
+ metadata: string;
1973
+ }> = [];
1974
+
1975
+ for (let j = 0; j < dedupedTexts.length; j++) {
1976
+ const record = dedupedRecords[j];
1977
+ const entry = buildIngestedEntry({
1978
+ source: `connector:${source}`,
1979
+ scope,
1980
+ text: dedupedTexts[j],
1981
+ vector: dedupedVectors[j],
1982
+ extraction: extractions[j],
1983
+ file: record.id,
1984
+ heading: record.title,
1985
+ coreSummary: coreSummaries[j],
1986
+ });
1987
+
1988
+ // Merge connector-specific metadata
1989
+ const meta = JSON.parse(entry.metadata);
1990
+ if (record.tags?.length) meta.connectorTags = record.tags;
1991
+ if (record.contentHash) meta.contentHash = record.contentHash;
1992
+ if (record.sourceMetadata) meta.sourceMetadata = record.sourceMetadata;
1993
+ entry.metadata = JSON.stringify(meta);
1994
+
1995
+ // Respect categoryHint / importanceHint from connector
1996
+ if (record.categoryHint) {
1997
+ entry.category = record.categoryHint;
1998
+ }
1999
+ if (record.importanceHint !== undefined) {
2000
+ entry.importance = record.importanceHint;
2001
+ }
2002
+
2003
+ toStore.push(entry);
2004
+ }
2005
+
2006
+ if (toStore.length > 0) {
2007
+ await store.storeBatch(toStore);
2008
+ result.chunksIngested += toStore.length;
2009
+ }
2010
+ } catch (err: unknown) {
2011
+ const msg = err instanceof Error ? err.message : String(err);
2012
+ result.errors.push(`Connector batch error: ${msg}`);
2013
+ }
2014
+ }
2015
+
2016
+ if (options.verbose) {
2017
+ console.log(` connector:${source}: ${records.length} records → ${result.chunksIngested} stored`);
2018
+ }
2019
+
2020
+ // GB-3: Write source heartbeat after connector ingest
2021
+ try {
2022
+ const { writeHeartbeat } = await import("./source-heartbeat.js");
2023
+ writeHeartbeat(source, result.chunksIngested, result.errors);
2024
+ } catch {
2025
+ // Heartbeat write is best-effort — don't break ingest on failure
2026
+ }
2027
+
2028
+ return result;
2029
+ }
2030
+
2031
+ // ============================================================================
2032
+ // Obsidian Vault Ingest (GB-1)
2033
+ // ============================================================================
2034
+
2035
+ /**
2036
+ * Scan an Obsidian vault and ingest its markdown files through the connector-v1 pipeline.
2037
+ *
2038
+ * Wrapper that:
2039
+ * 1. Validates the path is an Obsidian vault (.obsidian/ exists)
2040
+ * 2. Calls scanVault() to produce a ConnectorOutputV1 payload
2041
+ * 3. Feeds the payload to ingestConnectorFile()
2042
+ */
2043
+ export async function ingestObsidianVault(
2044
+ store: MemoryStore,
2045
+ embedder: Embedder,
2046
+ vaultPath: string,
2047
+ options: {
2048
+ verbose?: boolean;
2049
+ noDedup?: boolean;
2050
+ llm?: LLMClient | null;
2051
+ scopePrefix?: string;
2052
+ } = {},
2053
+ ): Promise<IngestResult> {
2054
+ const resolved = expandHome(vaultPath);
2055
+
2056
+ if (!isObsidianVault(resolved)) {
2057
+ return {
2058
+ source: "obsidian",
2059
+ filesProcessed: 0,
2060
+ chunksIngested: 0,
2061
+ chunksSkipped: 0,
2062
+ chunksDeduped: 0,
2063
+ dedupReasonCounts: createDedupReasonCounts(),
2064
+ errors: [`Not an Obsidian vault (missing .obsidian/ directory): ${resolved}`],
2065
+ };
2066
+ }
2067
+
2068
+ const output = scanVault(resolved, { scopePrefix: options.scopePrefix });
2069
+ const content = JSON.stringify(output);
2070
+
2071
+ if (options.verbose) {
2072
+ console.log(` obsidian: scanned ${output.records.length} notes from ${resolved}`);
2073
+ }
2074
+
2075
+ return ingestConnectorFile(store, embedder, content, options);
2076
+ }
@@ -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
  // ============================================================================
@@ -163,8 +164,11 @@ export class KGExtractor {
163
164
  prompt,
164
165
  );
165
166
  } catch (err) {
167
+ // Transport/API failure is NOT "zero triples" — propagate so callers can
168
+ // distinguish (backfill must not journal it as processed). The capture
169
+ // path's fire-and-forget .catch() keeps memory writes unaffected.
166
170
  logWarn(`[KG] LLM extraction failed: ${String(err)}`);
167
- return [];
171
+ throw err instanceof Error ? err : new Error(String(err));
168
172
  }
169
173
 
170
174
  if (!response?.triples || !Array.isArray(response.triples)) {