recallnest 1.4.1 → 2.2.1

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 (233) hide show
  1. package/.env +4 -0
  2. package/CLAUDE.md +7 -3
  3. package/README.md +202 -130
  4. package/README_CN.md +263 -190
  5. package/bin/recallnest.mjs +62 -0
  6. package/config.json +55 -0
  7. package/daily-recall.sh +23 -0
  8. package/integrations/setup-all.sh +31 -0
  9. package/logs/daily-recall.error.log +0 -0
  10. package/logs/daily-recall.log +30 -0
  11. package/logs/ingest-2026-04-11.log +110 -0
  12. package/logs/ingest-2026-04-12.log +90 -0
  13. package/logs/launchagent-stderr.log +0 -0
  14. package/logs/launchagent-stdout.log +0 -0
  15. package/package.json +10 -3
  16. package/src/access-tracker.ts +3 -2
  17. package/src/activity-counter.ts +109 -0
  18. package/src/adaptive-retrieval.ts +6 -5
  19. package/src/admission-control.ts +163 -0
  20. package/src/anchor-generator.ts +80 -0
  21. package/src/api-server.ts +10 -0
  22. package/src/audit-log.ts +122 -0
  23. package/src/auto-gc.ts +98 -29
  24. package/src/auto-recall.ts +33 -4
  25. package/src/capture-engine.ts +372 -23
  26. package/src/cascade-forget.ts +3 -2
  27. package/src/cli.ts +107 -5
  28. package/src/compat.ts +22 -0
  29. package/src/confidence-tracker.ts +210 -47
  30. package/src/conflict-store.ts +12 -3
  31. package/src/consolidation-engine.ts +296 -8
  32. package/src/context-collapse-renderer.ts +222 -0
  33. package/src/context-composer-stable-selection.ts +5 -1
  34. package/src/context-composer.ts +239 -4
  35. package/src/context-reconstructor.ts +404 -0
  36. package/src/conversation-importer.ts +441 -0
  37. package/src/data-checkup.ts +232 -0
  38. package/src/decay-engine.ts +84 -0
  39. package/src/distill-lock.ts +122 -0
  40. package/src/doctor.ts +8 -10
  41. package/src/dream-pipeline.ts +227 -0
  42. package/src/emotion-detector.ts +90 -0
  43. package/src/eval.ts +3 -2
  44. package/src/forget-engine.ts +269 -0
  45. package/src/frequency-tracker.ts +175 -0
  46. package/src/graph-export.ts +644 -0
  47. package/src/ingest.ts +36 -6
  48. package/src/interference-detector.ts +187 -0
  49. package/src/kg-extractor.ts +15 -24
  50. package/src/kg-store.ts +2 -1
  51. package/src/language-hook.ts +67 -0
  52. package/src/llm-client.ts +289 -6
  53. package/src/mcp-server.ts +707 -80
  54. package/src/memory-assets.ts +22 -5
  55. package/src/memory-evolution.ts +365 -0
  56. package/src/memory-lint.ts +429 -0
  57. package/src/memory-output.ts +69 -0
  58. package/src/memory-schema.ts +74 -2
  59. package/src/narrative-schema.ts +79 -0
  60. package/src/narrative-tagger.ts +164 -0
  61. package/src/pii-detector.ts +98 -0
  62. package/src/post-retrieval-filter.ts +76 -0
  63. package/src/prediction-engine.ts +253 -0
  64. package/src/preference-slots.ts +65 -2
  65. package/src/prospective-memory.ts +215 -0
  66. package/src/query-expander.ts +67 -0
  67. package/src/recall-governor.ts +183 -0
  68. package/src/retention-policy.ts +90 -0
  69. package/src/retriever.ts +828 -55
  70. package/src/rif.ts +34 -8
  71. package/src/runtime-config.ts +23 -8
  72. package/src/scope-inventory.ts +4 -3
  73. package/src/scope-policy.ts +4 -0
  74. package/src/session-distiller.ts +307 -0
  75. package/src/session-output.ts +40 -0
  76. package/src/session-schema.ts +37 -0
  77. package/src/session-store.ts +7 -2
  78. package/src/skill-engine.ts +208 -0
  79. package/src/skill-promotion.ts +293 -0
  80. package/src/skill-schema.ts +39 -0
  81. package/src/store.ts +96 -10
  82. package/src/topic-tag.ts +66 -0
  83. package/src/tracker.ts +3 -1
  84. package/src/ui-server.ts +80 -8
  85. package/src/workflow-observation-scope-review.ts +2 -1
  86. package/src/workflow-observation-store.ts +2 -1
  87. package/src/write-verifier.ts +126 -0
  88. package/.claude-plugin/CLAUDE.md +0 -29
  89. package/.claude-plugin/marketplace.json +0 -18
  90. package/.github/workflows/ci.yml +0 -28
  91. package/.mcp.json +0 -9
  92. package/CHANGELOG.md +0 -51
  93. package/DEMO.md +0 -94
  94. package/EVAL.md +0 -89
  95. package/FAILURES.md +0 -75
  96. package/OPERATIONS.md +0 -59
  97. package/ROADMAP.md +0 -198
  98. package/assets/recallnest-workbench.png +0 -0
  99. package/assets/ui/app.js +0 -807
  100. package/assets/ui/index.html +0 -154
  101. package/assets/ui/styles.css +0 -605
  102. package/assets/wechat-qr.jpg +0 -0
  103. package/daily-reflections/2026-03-05.md +0 -15
  104. package/daily-reflections/2026-03-06.md +0 -15
  105. package/daily-reflections/2026-03-07.md +0 -7
  106. package/daily-reflections/2026-03-08.md +0 -34
  107. package/daily-reflections/2026-03-09.md +0 -35
  108. package/daily-reflections/2026-03-10.md +0 -36
  109. package/daily-reflections/2026-03-11.md +0 -35
  110. package/daily-reflections/2026-03-12.md +0 -36
  111. package/daily-reflections/2026-03-13.md +0 -35
  112. package/daily-reflections/2026-03-14.md +0 -35
  113. package/daily-reflections/2026-03-15.md +0 -35
  114. package/daily-reflections/2026-03-16.md +0 -41
  115. package/daily-reflections/2026-03-17.md +0 -10
  116. package/daily-reflections/2026-03-18.md +0 -10
  117. package/daily-reflections/2026-03-19.md +0 -10
  118. package/daily-reflections/2026-03-20.md +0 -10
  119. package/daily-reflections/2026-03-21.md +0 -10
  120. package/daily-reflections/2026-03-22.md +0 -10
  121. package/daily-reflections/2026-03-23.md +0 -10
  122. package/daily-reflections/2026-03-24.md +0 -10
  123. package/daily-reflections/2026-03-25.md +0 -32
  124. package/daily-reflections/2026-03-26.md +0 -37
  125. package/daily-reflections/2026-03-27.md +0 -35
  126. package/daily-reflections/2026-03-28.md +0 -35
  127. package/daily-reflections/2026-03-29.md +0 -35
  128. package/daily-reflections/2026-03-30.md +0 -36
  129. package/daily-reflections/2026-03-31.md +0 -35
  130. package/daily-reflections/2026-04-01.md +0 -35
  131. package/docs/api-reference.md +0 -1291
  132. package/docs/architecture.md +0 -168
  133. package/docs/memory-boundary-contract.md +0 -226
  134. package/docs/memory-categories.md +0 -119
  135. package/docs/pipeline-audit-ultramemory.md +0 -177
  136. package/docs/plans/2026-03-08-query-expansion.md +0 -270
  137. package/docs/plans/2026-03-16-continuity-layer.md +0 -196
  138. package/docs/plans/mcp-tiering-plan.md +0 -126
  139. package/docs/research/recall-quality-audit-2026-03-22.md +0 -153
  140. package/docs/self-evolution.md +0 -198
  141. package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +0 -782
  142. package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +0 -77
  143. package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +0 -95
  144. package/eval/continuity/results-2026-03-16.md +0 -117
  145. package/eval/continuity/results-2026-03-19.md +0 -453
  146. package/eval/continuity/runbook.md +0 -110
  147. package/eval/continuity/seed-cases.md +0 -54
  148. package/eval/reports/2026-03-06-abstract-query-baseline.md +0 -96
  149. package/eval/reports/2026-03-06-aws-ssh-baseline.md +0 -82
  150. package/eval/reports/2026-03-06-baseline.md +0 -82
  151. package/eval/reports/query-expansion.md +0 -138
  152. package/lm +0 -19
  153. package/plan-v1.2-distributable.md +0 -105
  154. package/scripts/archive-distilled-junk.ts +0 -116
  155. package/scripts/backfill-l0l1l2.ts +0 -98
  156. package/scripts/blind-scan.ts +0 -588
  157. package/scripts/cleanup-step1-exact-dups.ts +0 -117
  158. package/scripts/cleanup-step2-template-dups.ts +0 -113
  159. package/scripts/cleanup-step3-stale-content.ts +0 -149
  160. package/scripts/cleanup-step4-orphan-scopes.ts +0 -131
  161. package/scripts/delete-distilled-junk.ts +0 -88
  162. package/scripts/distill-facts.ts +0 -305
  163. package/scripts/health-check.ts +0 -343
  164. package/scripts/incremental-ingest.sh +0 -71
  165. package/scripts/mlp-health-check.ts +0 -382
  166. package/scripts/pull-claude-desktop.ts +0 -515
  167. package/scripts/re-ingest-cc.sh +0 -66
  168. package/scripts/rebalance-memory-health.ts +0 -181
  169. package/scripts/run-desktop-ingest.sh +0 -11
  170. package/scripts/start-server.sh +0 -12
  171. package/scripts/viz-dashboard.html +0 -423
  172. package/scripts/viz-export.ts +0 -703
  173. package/src/__tests__/access-tracker-novelty.test.ts +0 -228
  174. package/src/__tests__/auto-consolidation.test.ts +0 -168
  175. package/src/__tests__/auto-recall.test.ts +0 -131
  176. package/src/__tests__/batch-dedup.test.ts +0 -65
  177. package/src/__tests__/capture-engine.test.ts +0 -1142
  178. package/src/__tests__/capture-heuristic.test.ts +0 -171
  179. package/src/__tests__/cascade-forget.test.ts +0 -210
  180. package/src/__tests__/category-min-scores.test.ts +0 -33
  181. package/src/__tests__/confidence-tracker.test.ts +0 -196
  182. package/src/__tests__/conflict-advisor.test.ts +0 -206
  183. package/src/__tests__/conflict-escalation.test.ts +0 -149
  184. package/src/__tests__/conflict-lifecycle.test.ts +0 -83
  185. package/src/__tests__/conflict-output.test.ts +0 -85
  186. package/src/__tests__/conflict-store.test.ts +0 -374
  187. package/src/__tests__/consolidation-engine.test.ts +0 -181
  188. package/src/__tests__/context-composer-pins.test.ts +0 -64
  189. package/src/__tests__/context-composer-task-selection.test.ts +0 -24
  190. package/src/__tests__/context-renderer.test.ts +0 -91
  191. package/src/__tests__/continuity-eval.test.ts +0 -275
  192. package/src/__tests__/continuity-schema.test.ts +0 -97
  193. package/src/__tests__/core-recall-eval.test.ts +0 -235
  194. package/src/__tests__/core-summary.test.ts +0 -109
  195. package/src/__tests__/dedup-actions.test.ts +0 -58
  196. package/src/__tests__/deterministic-id.test.ts +0 -39
  197. package/src/__tests__/doctor.test.ts +0 -303
  198. package/src/__tests__/drill-down.test.ts +0 -108
  199. package/src/__tests__/embedder.test.ts +0 -122
  200. package/src/__tests__/entity-resolver.test.ts +0 -58
  201. package/src/__tests__/event-segmenter.test.ts +0 -153
  202. package/src/__tests__/hotness-blend.test.ts +0 -76
  203. package/src/__tests__/ingest.test.ts +0 -360
  204. package/src/__tests__/kg-extractor.test.ts +0 -184
  205. package/src/__tests__/kg-store.test.ts +0 -198
  206. package/src/__tests__/llm-consolidation.test.ts +0 -226
  207. package/src/__tests__/memory-boundaries.test.ts +0 -237
  208. package/src/__tests__/memory-health-rebalance.test.ts +0 -144
  209. package/src/__tests__/memory-output.test.ts +0 -215
  210. package/src/__tests__/multi-vector.test.ts +0 -242
  211. package/src/__tests__/ppr-traversal.test.ts +0 -256
  212. package/src/__tests__/preference-guard-smoke.test.ts +0 -211
  213. package/src/__tests__/preference-matcher.test.ts +0 -230
  214. package/src/__tests__/preference-slots.test.ts +0 -106
  215. package/src/__tests__/prospective-memory.test.ts +0 -283
  216. package/src/__tests__/query-entity-detector.test.ts +0 -171
  217. package/src/__tests__/query-expander.test.ts +0 -34
  218. package/src/__tests__/recall-mode.test.ts +0 -64
  219. package/src/__tests__/result-synthesizer.test.ts +0 -198
  220. package/src/__tests__/resume-context.test.ts +0 -4743
  221. package/src/__tests__/retrieval-trace.test.ts +0 -213
  222. package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
  223. package/src/__tests__/retroactive-boost.test.ts +0 -223
  224. package/src/__tests__/scope-inventory.test.ts +0 -200
  225. package/src/__tests__/scope-policy.test.ts +0 -69
  226. package/src/__tests__/session-checkpoint.test.ts +0 -259
  227. package/src/__tests__/session-gc.test.ts +0 -215
  228. package/src/__tests__/store-scope-audit.test.ts +0 -81
  229. package/src/__tests__/temporal-parser.test.ts +0 -241
  230. package/src/__tests__/term-registry.test.ts +0 -121
  231. package/src/__tests__/tool-output-compressor.test.ts +0 -133
  232. package/src/__tests__/version-manager.test.ts +0 -213
  233. package/src/__tests__/workflow-observation.test.ts +0 -400
package/src/cli.ts CHANGED
@@ -14,6 +14,7 @@ import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
14
14
  import { join, resolve } from "node:path";
15
15
  import { homedir } from "node:os";
16
16
 
17
+ import { metaDir } from "./compat.js";
17
18
  import type { RetrievalResult } from "./retriever.js";
18
19
  import { applyRetrievalProfile, listRetrievalProfiles } from "./retrieval-profiles.js";
19
20
  import { distillResults, formatExplainResults, formatSearchResults, selectBriefSeedResults, summarizeResults } from "./memory-output.js";
@@ -280,19 +281,19 @@ function filterConflictsByAttention<T extends { status: string; createdAt: strin
280
281
  function workflowPatternSeedsPath(file?: string): string {
281
282
  return file
282
283
  ? resolve(file)
283
- : resolve(import.meta.dir, "../eval/continuity/pattern-seeds.json");
284
+ : resolve(metaDir(import.meta), "../eval/continuity/pattern-seeds.json");
284
285
  }
285
286
 
286
287
  function caseMemorySeedsPath(file?: string): string {
287
288
  return file
288
289
  ? resolve(file)
289
- : resolve(import.meta.dir, "../eval/continuity/case-seeds.json");
290
+ : resolve(metaDir(import.meta), "../eval/continuity/case-seeds.json");
290
291
  }
291
292
 
292
293
  function memorySeedsPath(file?: string): string {
293
294
  return file
294
295
  ? resolve(file)
295
- : resolve(import.meta.dir, "../eval/continuity/memory-seeds.json");
296
+ : resolve(metaDir(import.meta), "../eval/continuity/memory-seeds.json");
296
297
  }
297
298
 
298
299
  function parseMetadata(raw?: string): Record<string, unknown> {
@@ -1409,7 +1410,7 @@ program
1409
1410
  if (source === "all" || source === "desktop") {
1410
1411
  const desktopSource = config.sources.desktop;
1411
1412
  if (desktopSource) {
1412
- const desktopPath = resolve(import.meta.dir, "..", desktopSource.path);
1413
+ const desktopPath = resolve(metaDir(import.meta), "..", desktopSource.path);
1413
1414
  if (existsSync(desktopPath)) {
1414
1415
  console.log("🖥️ 导入 Claude Desktop 对话...");
1415
1416
  const r = await ingestCCTranscripts(store, embedder, desktopPath, ingestOpts);
@@ -1580,7 +1581,7 @@ program
1580
1581
  }
1581
1582
 
1582
1583
  const config = loadConfig();
1583
- const dbPath = resolve(import.meta.dir, "..", expandHome(config.dbPath));
1584
+ const dbPath = resolve(metaDir(import.meta), "..", expandHome(config.dbPath));
1584
1585
 
1585
1586
  if (existsSync(dbPath)) {
1586
1587
  const { rmSync } = await import("node:fs");
@@ -1898,6 +1899,51 @@ program
1898
1899
  }
1899
1900
  });
1900
1901
 
1902
+ program
1903
+ .command("import <path>")
1904
+ .description("导入多格式对话文件到记忆索引")
1905
+ .option("-f, --format <format>", "格式: auto|claude-code|chatgpt|slack|claude-ai|plaintext", "auto")
1906
+ .option("-s, --scope <scope>", "目标 scope(必须)")
1907
+ .action(async (filePath: string, options) => {
1908
+ if (!options.scope) {
1909
+ console.error("❌ --scope is required");
1910
+ process.exitCode = 1;
1911
+ return;
1912
+ }
1913
+ const resolvedPath = resolve(filePath);
1914
+ if (!existsSync(resolvedPath)) {
1915
+ console.error(`❌ File not found: ${resolvedPath}`);
1916
+ process.exitCode = 1;
1917
+ return;
1918
+ }
1919
+ const validFormats = ["auto", "claude-code", "chatgpt", "slack", "claude-ai", "plaintext"] as const;
1920
+ if (!validFormats.includes(options.format)) {
1921
+ console.error(`❌ Invalid format: ${options.format}. Must be one of: ${validFormats.join(", ")}`);
1922
+ process.exitCode = 1;
1923
+ return;
1924
+ }
1925
+
1926
+ const config = loadConfig();
1927
+ const { store, embedder, llm } = createComponents(config);
1928
+
1929
+ const { importConversation } = await import("./conversation-importer.js");
1930
+ const result = await importConversation(
1931
+ { store, embedder, llm },
1932
+ resolvedPath,
1933
+ options.scope,
1934
+ options.format as "auto" | "claude-code" | "chatgpt" | "slack" | "claude-ai" | "plaintext",
1935
+ );
1936
+
1937
+ console.log(`\n📥 Import complete`);
1938
+ console.log(` Total messages: ${result.total}`);
1939
+ console.log(` Stored: ${result.stored}`);
1940
+ console.log(` Rejected: ${result.rejected}`);
1941
+ if (result.errors.length > 0) {
1942
+ console.log(` Errors: ${result.errors.length}`);
1943
+ for (const e of result.errors) console.log(` ${e}`);
1944
+ }
1945
+ });
1946
+
1901
1947
  program
1902
1948
  .command("doctor")
1903
1949
  .description("检查安装状态和配置")
@@ -1909,6 +1955,62 @@ program
1909
1955
  if (hasFailure) process.exitCode = 1;
1910
1956
  });
1911
1957
 
1958
+ program
1959
+ .command("lint")
1960
+ .description("Run memory quality lint checks")
1961
+ .option("--scope <scope>", "Limit to a specific scope")
1962
+ .option("--json", "Output raw JSON instead of formatted report")
1963
+ .option("--verbose", "Show all individual findings")
1964
+ .action(async (options) => {
1965
+ const { runMemoryLint, formatMemoryLintReport } = await import("./memory-lint.js");
1966
+ const config = loadConfig();
1967
+ const { store } = createComponents(config);
1968
+
1969
+ const report = await runMemoryLint({
1970
+ store,
1971
+ scope: options.scope,
1972
+ verbose: options.verbose,
1973
+ });
1974
+
1975
+ if (options.json) {
1976
+ console.log(JSON.stringify(report, null, 2));
1977
+ } else {
1978
+ console.log();
1979
+ console.log(formatMemoryLintReport(report));
1980
+ }
1981
+ });
1982
+
1983
+ program
1984
+ .command("graph")
1985
+ .description("Export interactive knowledge graph as HTML")
1986
+ .option("--scope <scope>", "Limit to a specific scope")
1987
+ .option("--max-nodes <n>", "Maximum nodes (default 200)", "200")
1988
+ .option("--out <path>", "Output file path")
1989
+ .option("--open", "Open in browser after export")
1990
+ .action(async (options) => {
1991
+ const { exportMemoryGraph, formatGraphExportResult } = await import("./graph-export.js");
1992
+ const config = loadConfig();
1993
+ const { store } = createComponents(config);
1994
+
1995
+ const maxNodes = parseInt(options.maxNodes, 10) || 200;
1996
+ const { path, graph } = await exportMemoryGraph(store, {
1997
+ scope: options.scope,
1998
+ maxNodes,
1999
+ outputPath: options.out,
2000
+ });
2001
+
2002
+ console.log();
2003
+ console.log(formatGraphExportResult(path, graph));
2004
+
2005
+ if (options.open) {
2006
+ const { execFileSync } = await import("node:child_process");
2007
+ try {
2008
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
2009
+ execFileSync(cmd, [path], { stdio: "ignore" });
2010
+ } catch {}
2011
+ }
2012
+ });
2013
+
1912
2014
  // Run
1913
2015
  loadDotEnv();
1914
2016
  program.parseAsync(process.argv).catch((error) => {
package/src/compat.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Cross-runtime compatibility helpers
3
+ *
4
+ * Bridges Bun-specific APIs to work with Node.js as well.
5
+ */
6
+ import { dirname } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ /**
10
+ * Cross-runtime replacement for Bun's `import.meta.dir`.
11
+ * - Bun: returns import.meta.dir (native, fast)
12
+ * - Node.js: derives from import.meta.url
13
+ *
14
+ * Usage: replace `import.meta.dir` with `metaDir(import.meta)`
15
+ */
16
+ export function metaDir(meta: ImportMeta): string {
17
+ // Bun sets import.meta.dir natively
18
+ const dir = (meta as Record<string, unknown>).dir;
19
+ if (typeof dir === "string") return dir;
20
+ // Node.js fallback
21
+ return dirname(fileURLToPath(meta.url));
22
+ }
@@ -19,9 +19,13 @@
19
19
  *
20
20
  * Retrieval integration: confidence multiplies into the score pipeline
21
21
  * as `score *= (0.5 + 0.5 * confidence)` — same pattern as importance.
22
+ *
23
+ * Design: returns metadata patches instead of directly mutating the store.
24
+ * Callers apply patches via their own store.update() / patchMetadata().
25
+ * This keeps the module store-agnostic and easier to test.
22
26
  */
23
27
 
24
- import type { MemoryStore, MemoryEntry } from "./store.js";
28
+ import type { MemoryEntry } from "./store.js";
25
29
 
26
30
  // ---------------------------------------------------------------------------
27
31
  // Constants
@@ -32,24 +36,60 @@ export const CONFIDENCE_CONFIRMED = 1.0;
32
36
  export const CONFIDENCE_CORRECTED = 0.3;
33
37
  export const CONFIDENCE_CONTRADICTED = 0.0;
34
38
 
39
+ /** Max history entries to keep per memory (prevents unbounded growth). */
40
+ const MAX_CONFIDENCE_HISTORY = 20;
41
+
35
42
  // ---------------------------------------------------------------------------
36
- // Helpers
43
+ // F1: Structured Confidence Metadata
37
44
  // ---------------------------------------------------------------------------
38
45
 
39
- function parseMeta(entry: MemoryEntry): Record<string, any> {
40
- try { return JSON.parse(entry.metadata || "{}"); } catch { return {}; }
46
+ export type ConfidenceReliability = "direct" | "inferred" | "hearsay";
47
+
48
+ export interface ConfidenceMetadata {
49
+ /** 0–1 composite confidence score. */
50
+ score: number;
51
+ /** Source reliability tier. */
52
+ reliability: ConfidenceReliability;
53
+ /** Last verification timestamp (ms). */
54
+ verifiedAt?: number;
55
+ /** Who verified: user / system / llm. */
56
+ verifiedBy?: string;
41
57
  }
42
58
 
43
59
  /**
44
- * Read confidence from metadata. Returns default (0.7) if not set.
60
+ * F1: Assign default confidence based on memory source.
61
+ * Returns a ConfidenceMetadata object to embed in metadata JSON.
45
62
  */
46
- export function getConfidence(entry: MemoryEntry): number {
47
- const meta = parseMeta(entry);
48
- return typeof meta.confidence === "number" ? meta.confidence : CONFIDENCE_DEFAULT;
63
+ export function assignDefaultConfidence(
64
+ source: string,
65
+ explicitConfidence?: Partial<ConfidenceMetadata>,
66
+ ): ConfidenceMetadata {
67
+ // If caller provided explicit values, use them (fill in defaults for missing fields)
68
+ if (explicitConfidence?.score != null) {
69
+ return {
70
+ score: explicitConfidence.score,
71
+ reliability: explicitConfidence.reliability ?? "inferred",
72
+ verifiedAt: explicitConfidence.verifiedAt,
73
+ verifiedBy: explicitConfidence.verifiedBy,
74
+ };
75
+ }
76
+ switch (source) {
77
+ case "manual":
78
+ return { score: 0.9, reliability: "direct" };
79
+ case "agent":
80
+ case "api":
81
+ return { score: 0.7, reliability: "inferred" };
82
+ case "session_distill":
83
+ return { score: 0.6, reliability: "inferred" };
84
+ case "conversation_import":
85
+ return { score: 0.5, reliability: "hearsay" };
86
+ default:
87
+ return { score: CONFIDENCE_DEFAULT, reliability: "inferred" };
88
+ }
49
89
  }
50
90
 
51
91
  // ---------------------------------------------------------------------------
52
- // Core operations
92
+ // Types
53
93
  // ---------------------------------------------------------------------------
54
94
 
55
95
  export interface ConfidenceUpdate {
@@ -58,46 +98,118 @@ export interface ConfidenceUpdate {
58
98
  newConfidence: number;
59
99
  }
60
100
 
101
+ export interface ConfidencePatch {
102
+ /** The metadata object to merge/write back. */
103
+ metadata: Record<string, unknown>;
104
+ /** Summary of what changed. */
105
+ update: ConfidenceUpdate;
106
+ }
107
+
108
+ interface ConfidenceHistoryEntry {
109
+ action: "confirmed" | "corrected" | "contradicted";
110
+ from: number;
111
+ to: number;
112
+ date: string;
113
+ correctedBy?: string;
114
+ }
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // Helpers
118
+ // ---------------------------------------------------------------------------
119
+
120
+ function parseMeta(entry: MemoryEntry): Record<string, unknown> {
121
+ try { return JSON.parse(entry.metadata || "{}"); } catch { return {}; }
122
+ }
123
+
61
124
  /**
62
- * Mark a memory as confirmed (user validated it).
63
- * Bumps confidence to CONFIRMED (1.0).
125
+ * Read confidence score from metadata. Handles both flat number (legacy)
126
+ * and structured ConfidenceMetadata object (F1).
127
+ * Returns default (0.7) if not set.
64
128
  */
65
- export async function confirmMemory(
66
- store: MemoryStore,
67
- entryId: string,
68
- scope: string,
69
- ): Promise<ConfidenceUpdate | null> {
70
- const entry = await store.getById(entryId);
129
+ export function getConfidence(entry: MemoryEntry): number {
130
+ const meta = parseMeta(entry);
131
+ if (typeof meta.confidence === "number") return meta.confidence;
132
+ if (meta.confidence && typeof meta.confidence === "object") {
133
+ const obj = meta.confidence as Record<string, unknown>;
134
+ if (typeof obj.score === "number") return obj.score;
135
+ }
136
+ return CONFIDENCE_DEFAULT;
137
+ }
138
+
139
+ /**
140
+ * F1: Read full ConfidenceMetadata from entry. Returns null if not set.
141
+ */
142
+ export function getConfidenceMetadata(entry: MemoryEntry): ConfidenceMetadata | null {
143
+ const meta = parseMeta(entry);
144
+ if (meta.confidence && typeof meta.confidence === "object") {
145
+ const obj = meta.confidence as Record<string, unknown>;
146
+ if (typeof obj.score === "number") {
147
+ return {
148
+ score: obj.score,
149
+ reliability: (obj.reliability as ConfidenceReliability) ?? "inferred",
150
+ verifiedAt: typeof obj.verifiedAt === "number" ? obj.verifiedAt : undefined,
151
+ verifiedBy: typeof obj.verifiedBy === "string" ? obj.verifiedBy : undefined,
152
+ };
153
+ }
154
+ }
155
+ // Legacy flat number → wrap
156
+ if (typeof meta.confidence === "number") {
157
+ return { score: meta.confidence, reliability: "inferred" };
158
+ }
159
+ return null;
160
+ }
161
+
162
+ function appendHistory(
163
+ meta: Record<string, unknown>,
164
+ entry: ConfidenceHistoryEntry,
165
+ ): void {
166
+ if (!Array.isArray(meta.confidence_history)) meta.confidence_history = [];
167
+ const history = meta.confidence_history as ConfidenceHistoryEntry[];
168
+ history.push(entry);
169
+ // Cap history length
170
+ if (history.length > MAX_CONFIDENCE_HISTORY) {
171
+ meta.confidence_history = history.slice(-MAX_CONFIDENCE_HISTORY);
172
+ }
173
+ }
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // Patch builders (store-agnostic — return patches, don't write)
177
+ // ---------------------------------------------------------------------------
178
+
179
+ /**
180
+ * Build a patch to mark a memory as confirmed (user validated it).
181
+ * Returns null if entry is null (not found).
182
+ */
183
+ export function buildConfirmPatch(
184
+ entry: MemoryEntry | null,
185
+ ): ConfidencePatch | null {
71
186
  if (!entry) return null;
72
187
 
73
188
  const meta = parseMeta(entry);
74
189
  const oldConfidence = typeof meta.confidence === "number" ? meta.confidence : CONFIDENCE_DEFAULT;
75
190
 
76
191
  meta.confidence = CONFIDENCE_CONFIRMED;
77
- if (!Array.isArray(meta.confidence_history)) meta.confidence_history = [];
78
- meta.confidence_history.push({
192
+ appendHistory(meta, {
79
193
  action: "confirmed",
80
194
  from: oldConfidence,
81
195
  to: CONFIDENCE_CONFIRMED,
82
196
  date: new Date().toISOString().slice(0, 10),
83
197
  });
84
198
 
85
- await store.update(entryId, { metadata: JSON.stringify(meta) }, [scope]);
86
-
87
- return { entryId, oldConfidence, newConfidence: CONFIDENCE_CONFIRMED };
199
+ return {
200
+ metadata: meta,
201
+ update: { entryId: entry.id, oldConfidence, newConfidence: CONFIDENCE_CONFIRMED },
202
+ };
88
203
  }
89
204
 
90
205
  /**
91
- * Mark a memory as corrected (user provided updated info).
92
- * Drops confidence to CORRECTED (0.3).
206
+ * Build a patch to mark a memory as corrected (user provided updated info).
207
+ * Returns null if entry is null (not found).
93
208
  */
94
- export async function correctMemory(
95
- store: MemoryStore,
96
- entryId: string,
97
- scope: string,
209
+ export function buildCorrectPatch(
210
+ entry: MemoryEntry | null,
98
211
  correctedById?: string,
99
- ): Promise<ConfidenceUpdate | null> {
100
- const entry = await store.getById(entryId);
212
+ ): ConfidencePatch | null {
101
213
  if (!entry) return null;
102
214
 
103
215
  const meta = parseMeta(entry);
@@ -105,8 +217,7 @@ export async function correctMemory(
105
217
 
106
218
  meta.confidence = CONFIDENCE_CORRECTED;
107
219
  if (correctedById) meta.corrected_by = correctedById;
108
- if (!Array.isArray(meta.confidence_history)) meta.confidence_history = [];
109
- meta.confidence_history.push({
220
+ appendHistory(meta, {
110
221
  action: "corrected",
111
222
  from: oldConfidence,
112
223
  to: CONFIDENCE_CORRECTED,
@@ -114,40 +225,92 @@ export async function correctMemory(
114
225
  date: new Date().toISOString().slice(0, 10),
115
226
  });
116
227
 
117
- await store.update(entryId, { metadata: JSON.stringify(meta) }, [scope]);
118
-
119
- return { entryId, oldConfidence, newConfidence: CONFIDENCE_CORRECTED };
228
+ return {
229
+ metadata: meta,
230
+ update: { entryId: entry.id, oldConfidence, newConfidence: CONFIDENCE_CORRECTED },
231
+ };
120
232
  }
121
233
 
122
234
  /**
123
- * Mark a memory as contradicted (explicitly wrong).
124
- * Drops confidence to CONTRADICTED (0.0).
235
+ * Build a patch to mark a memory as contradicted (explicitly wrong).
236
+ * Returns null if entry is null (not found).
125
237
  */
126
- export async function contradictMemory(
127
- store: MemoryStore,
128
- entryId: string,
129
- scope: string,
130
- ): Promise<ConfidenceUpdate | null> {
131
- const entry = await store.getById(entryId);
238
+ export function buildContradictPatch(
239
+ entry: MemoryEntry | null,
240
+ ): ConfidencePatch | null {
132
241
  if (!entry) return null;
133
242
 
134
243
  const meta = parseMeta(entry);
135
244
  const oldConfidence = typeof meta.confidence === "number" ? meta.confidence : CONFIDENCE_DEFAULT;
136
245
 
137
246
  meta.confidence = CONFIDENCE_CONTRADICTED;
138
- if (!Array.isArray(meta.confidence_history)) meta.confidence_history = [];
139
- meta.confidence_history.push({
247
+ appendHistory(meta, {
140
248
  action: "contradicted",
141
249
  from: oldConfidence,
142
250
  to: CONFIDENCE_CONTRADICTED,
143
251
  date: new Date().toISOString().slice(0, 10),
144
252
  });
145
253
 
146
- await store.update(entryId, { metadata: JSON.stringify(meta) }, [scope]);
254
+ return {
255
+ metadata: meta,
256
+ update: { entryId: entry.id, oldConfidence, newConfidence: CONFIDENCE_CONTRADICTED },
257
+ };
258
+ }
259
+
260
+ // ---------------------------------------------------------------------------
261
+ // Convenience wrappers (for callers who DO have a store)
262
+ // ---------------------------------------------------------------------------
263
+
264
+ /**
265
+ * Confirm a memory and write back to store. Convenience wrapper.
266
+ */
267
+ export async function confirmMemory(
268
+ store: { getById(id: string): Promise<MemoryEntry | null>; update(id: string, data: { metadata: string }, scopes: string[]): Promise<void> },
269
+ entryId: string,
270
+ scope: string,
271
+ ): Promise<ConfidenceUpdate | null> {
272
+ const entry = await store.getById(entryId);
273
+ const patch = buildConfirmPatch(entry);
274
+ if (!patch) return null;
275
+ await store.update(entryId, { metadata: JSON.stringify(patch.metadata) }, [scope]);
276
+ return patch.update;
277
+ }
278
+
279
+ /**
280
+ * Correct a memory and write back to store. Convenience wrapper.
281
+ */
282
+ export async function correctMemory(
283
+ store: { getById(id: string): Promise<MemoryEntry | null>; update(id: string, data: { metadata: string }, scopes: string[]): Promise<void> },
284
+ entryId: string,
285
+ scope: string,
286
+ correctedById?: string,
287
+ ): Promise<ConfidenceUpdate | null> {
288
+ const entry = await store.getById(entryId);
289
+ const patch = buildCorrectPatch(entry, correctedById);
290
+ if (!patch) return null;
291
+ await store.update(entryId, { metadata: JSON.stringify(patch.metadata) }, [scope]);
292
+ return patch.update;
293
+ }
147
294
 
148
- return { entryId, oldConfidence, newConfidence: CONFIDENCE_CONTRADICTED };
295
+ /**
296
+ * Contradict a memory and write back to store. Convenience wrapper.
297
+ */
298
+ export async function contradictMemory(
299
+ store: { getById(id: string): Promise<MemoryEntry | null>; update(id: string, data: { metadata: string }, scopes: string[]): Promise<void> },
300
+ entryId: string,
301
+ scope: string,
302
+ ): Promise<ConfidenceUpdate | null> {
303
+ const entry = await store.getById(entryId);
304
+ const patch = buildContradictPatch(entry);
305
+ if (!patch) return null;
306
+ await store.update(entryId, { metadata: JSON.stringify(patch.metadata) }, [scope]);
307
+ return patch.update;
149
308
  }
150
309
 
310
+ // ---------------------------------------------------------------------------
311
+ // Retrieval integration (unchanged — already store-agnostic)
312
+ // ---------------------------------------------------------------------------
313
+
151
314
  /**
152
315
  * Apply confidence weighting to retrieval scores.
153
316
  * Formula: score *= (0.5 + 0.5 * confidence)
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
 
4
+ import { metaDir } from "./compat.js";
4
5
  import type { ConflictCandidateRecord, ConflictStatus } from "./conflict-schema.js";
5
6
  import { ConflictCandidateRecordSchema } from "./conflict-schema.js";
6
7
 
@@ -24,7 +25,7 @@ function sortNewestFirst(records: ConflictCandidateRecord[]): ConflictCandidateR
24
25
  }
25
26
 
26
27
  export class ConflictCandidateStore {
27
- constructor(private readonly dir = resolve(import.meta.dir, "../data/conflict-candidates")) {}
28
+ constructor(private readonly dir = resolve(metaDir(import.meta), "../data/conflict-candidates")) {}
28
29
 
29
30
  get dataDir(): string {
30
31
  return ensureDir(this.dir);
@@ -34,7 +35,11 @@ export class ConflictCandidateStore {
34
35
  const parsed = ConflictCandidateRecordSchema.parse(record);
35
36
  const timestampToken = parsed.createdAt.replace(/[:.]/g, "-");
36
37
  const path = join(this.dataDir, `${timestampToken}-${parsed.conflictId}.json`);
37
- writeFileSync(path, JSON.stringify(parsed, null, 2) + "\n");
38
+ try {
39
+ writeFileSync(path, JSON.stringify(parsed, null, 2) + "\n");
40
+ } catch (err) {
41
+ console.error("[recallnest] Failed to save conflict record:", err instanceof Error ? err.message : String(err));
42
+ }
38
43
  return parsed;
39
44
  }
40
45
 
@@ -43,7 +48,11 @@ export class ConflictCandidateStore {
43
48
  const existingPath = this.findPathById(parsed.conflictId);
44
49
  const timestampToken = parsed.createdAt.replace(/[:.]/g, "-");
45
50
  const path = existingPath || join(this.dataDir, `${timestampToken}-${parsed.conflictId}.json`);
46
- writeFileSync(path, JSON.stringify(parsed, null, 2) + "\n");
51
+ try {
52
+ writeFileSync(path, JSON.stringify(parsed, null, 2) + "\n");
53
+ } catch (err) {
54
+ console.error("[recallnest] Failed to replace conflict record:", err instanceof Error ? err.message : String(err));
55
+ }
47
56
  return parsed;
48
57
  }
49
58