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
@@ -10,6 +10,8 @@
10
10
  * No LLM required — pure math + access statistics.
11
11
  */
12
12
 
13
+ import type { EmotionMetadata } from "./memory-schema.js";
14
+
13
15
  // ============================================================================
14
16
  // Tier Definitions
15
17
  // ============================================================================
@@ -86,6 +88,88 @@ export function weibullDecay(
86
88
  return floor + (1 - floor) * decay;
87
89
  }
88
90
 
91
+ // ============================================================================
92
+ // Emotion-Adjusted Decay
93
+ // ============================================================================
94
+
95
+ /**
96
+ * Adjust half-life based on emotional intensity.
97
+ * Strong emotion extends half-life by up to 30%.
98
+ */
99
+ export function adjustHalfLifeForEmotion(
100
+ baseHalfLife: number,
101
+ emotion: EmotionMetadata | null | undefined,
102
+ ): number {
103
+ if (!emotion) return baseHalfLife;
104
+ const raw = Math.abs(emotion.valence);
105
+ // Dead-zone: ignore negligible emotional signal below threshold
106
+ const intensity = raw < 0.1 ? 0 : raw;
107
+ return baseHalfLife * (1 + 0.3 * intensity);
108
+ }
109
+
110
+ /**
111
+ * Compute initial strength boost from arousal (flashbulb memory effect).
112
+ * Returns multiplier in [1.0, 1.1].
113
+ */
114
+ export function computeArousalBoost(
115
+ emotion: EmotionMetadata | null | undefined,
116
+ ): number {
117
+ if (!emotion) return 1.0;
118
+ return 1 + 0.1 * emotion.arousal;
119
+ }
120
+
121
+ // ============================================================================
122
+ // HP-7: Decay Exemptions
123
+ // ============================================================================
124
+
125
+ /**
126
+ * Check if a memory should be exempt from time decay.
127
+ * Exempt entries keep their original score without time-based penalties.
128
+ *
129
+ * Exemption rules:
130
+ * 1. Core tier + high importance (≥ 0.95) → identity/correction, must not fade
131
+ * 2. Recently accessed (within 7 days) → actively used, not stale
132
+ * 3. Pinned → user explicitly marked as persistent
133
+ */
134
+ export function isDecayExempt(
135
+ metadata: string | undefined,
136
+ importance: number,
137
+ ): boolean {
138
+ if (!metadata) return false;
139
+
140
+ try {
141
+ const meta = JSON.parse(metadata) as Record<string, unknown>;
142
+
143
+ // Rule 1: Core + very high importance
144
+ const tier = meta.tier ?? resolveTierFromMeta(meta);
145
+ if (tier === "core" && importance >= 0.95) return true;
146
+
147
+ // Rule 2: Accessed within last 7 days
148
+ const lastAccess = typeof meta.lastAccessedAt === "number" ? meta.lastAccessedAt : 0;
149
+ if (lastAccess > 0 && (Date.now() - lastAccess) < 7 * 86_400_000) return true;
150
+
151
+ // Rule 3: Pinned
152
+ const tags = Array.isArray(meta.tags) ? meta.tags : [];
153
+ if (tags.includes("pinned")) return true;
154
+
155
+ return false;
156
+ } catch {
157
+ return false;
158
+ }
159
+ }
160
+
161
+ /** Internal helper: resolve tier from parsed metadata (avoids re-parsing). */
162
+ function resolveTierFromMeta(meta: Record<string, unknown>): MemoryTier {
163
+ if (meta.tier === "core" || meta.tier === "working" || meta.tier === "peripheral") {
164
+ return meta.tier;
165
+ }
166
+ const imp = typeof meta.importance === "number" ? meta.importance : 0;
167
+ const ac = typeof meta.accessCount === "number" ? meta.accessCount : 0;
168
+ if (imp >= 0.95 || ac >= 10) return "core";
169
+ if (imp >= 0.8 || ac >= 3) return "working";
170
+ return "peripheral";
171
+ }
172
+
89
173
  // ============================================================================
90
174
  // Tier Resolution
91
175
  // ============================================================================
@@ -0,0 +1,122 @@
1
+ /**
2
+ * CC-6: Distill gate — PID-based lock file + session gate.
3
+ * Lock file mtime serves as lastDistillAt timestamp (no separate field).
4
+ *
5
+ * - acquireLock(): write current PID, check existing lock PID liveness + mtime expiry
6
+ * - releaseLock(): delete lock file (mtime naturally updates to now on success)
7
+ * - rollbackLock(previousMtime): on failure, rewind lock mtime so next run retries
8
+ * - shouldDistill(checkpointCount): session gate — checkpoint count < 3 → skip
9
+ * - getLastDistillTime(): read lock file mtime, return 0 if absent
10
+ */
11
+
12
+ import {
13
+ mkdirSync,
14
+ writeFileSync,
15
+ readFileSync,
16
+ unlinkSync,
17
+ statSync,
18
+ utimesSync,
19
+ existsSync,
20
+ } from "node:fs";
21
+ import { join, dirname } from "node:path";
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Config
25
+ // ---------------------------------------------------------------------------
26
+
27
+ export interface DistillLockConfig {
28
+ /** Path to the lock file (default: data/distill.lock) */
29
+ lockPath: string;
30
+ /** Lock expiry in ms (default: 3600000 = 1h) */
31
+ expireMs: number;
32
+ /** Minimum checkpoint count before distill is allowed (default: 3) */
33
+ minCheckpoints: number;
34
+ }
35
+
36
+ export const DEFAULT_DISTILL_LOCK_CONFIG: DistillLockConfig = {
37
+ lockPath: join(process.env.RECALLNEST_DATA_DIR || "data", "distill.lock"),
38
+ expireMs: 3_600_000,
39
+ minCheckpoints: 3,
40
+ };
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Helpers
44
+ // ---------------------------------------------------------------------------
45
+
46
+ function resolveConfig(cfg?: Partial<DistillLockConfig>): DistillLockConfig {
47
+ return { ...DEFAULT_DISTILL_LOCK_CONFIG, ...cfg };
48
+ }
49
+
50
+ function isPidAlive(pid: number): boolean {
51
+ try {
52
+ process.kill(pid, 0);
53
+ return true;
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Public API
61
+ // ---------------------------------------------------------------------------
62
+
63
+ /**
64
+ * Try to acquire the distill lock.
65
+ * Returns true if lock was acquired, false if another live process holds it.
66
+ */
67
+ export function acquireLock(cfg?: Partial<DistillLockConfig>): boolean {
68
+ const { lockPath, expireMs } = resolveConfig(cfg);
69
+
70
+ if (existsSync(lockPath)) {
71
+ const existingPid = parseInt(readFileSync(lockPath, "utf-8").trim(), 10);
72
+ const mtime = statSync(lockPath).mtimeMs;
73
+ const expired = Date.now() - mtime > expireMs;
74
+
75
+ if (!isNaN(existingPid) && isPidAlive(existingPid) && !expired) {
76
+ return false;
77
+ }
78
+ // PID dead or lock expired — overwrite
79
+ }
80
+
81
+ mkdirSync(dirname(lockPath), { recursive: true });
82
+ writeFileSync(lockPath, String(process.pid), "utf-8");
83
+ return true;
84
+ }
85
+
86
+ /** Release the distill lock by removing the lock file. */
87
+ export function releaseLock(cfg?: Partial<DistillLockConfig>): void {
88
+ const { lockPath } = resolveConfig(cfg);
89
+ if (existsSync(lockPath)) {
90
+ unlinkSync(lockPath);
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Roll back lock mtime to a previous value so the next run retries sooner.
96
+ * Used when distill fails and we want to preserve the "last success" timestamp.
97
+ */
98
+ export function rollbackLock(
99
+ previousMtime: Date,
100
+ cfg?: Partial<DistillLockConfig>,
101
+ ): void {
102
+ const { lockPath } = resolveConfig(cfg);
103
+ if (existsSync(lockPath)) {
104
+ utimesSync(lockPath, previousMtime, previousMtime);
105
+ }
106
+ }
107
+
108
+ /** Session gate: returns true when enough checkpoints have accumulated. */
109
+ export function shouldDistill(
110
+ checkpointCountSinceLastDistill: number,
111
+ cfg?: Partial<DistillLockConfig>,
112
+ ): boolean {
113
+ const { minCheckpoints } = resolveConfig(cfg);
114
+ return checkpointCountSinceLastDistill >= minCheckpoints;
115
+ }
116
+
117
+ /** Read lock file mtime as the last-distill timestamp. Returns 0 if absent. */
118
+ export function getLastDistillTime(cfg?: Partial<DistillLockConfig>): number {
119
+ const { lockPath } = resolveConfig(cfg);
120
+ if (!existsSync(lockPath)) return 0;
121
+ return statSync(lockPath).mtimeMs;
122
+ }
package/src/doctor.ts CHANGED
@@ -13,6 +13,7 @@
13
13
  import { existsSync, accessSync, constants, readFileSync } from "node:fs";
14
14
  import { resolve, join } from "node:path";
15
15
  import { homedir } from "node:os";
16
+ import { metaDir } from "./compat.js";
16
17
  import { loadConfig, expandHome, resolveEnv, findConfigPath, loadDotEnv } from "./runtime-config.js";
17
18
  import { createEmbedder, type EmbeddingConfig } from "./embedder.js";
18
19
  import { extractCanonicalKey, normalizeCanonicalKey } from "./memory-boundaries.js";
@@ -96,15 +97,15 @@ export function assessScopeInventoryReport(report: ScopeInventoryReport): CheckR
96
97
  }
97
98
 
98
99
  function workflowPatternSeedsPath(): string {
99
- return resolve(import.meta.dir, "../eval/continuity/pattern-seeds.json");
100
+ return resolve(metaDir(import.meta), "../eval/continuity/pattern-seeds.json");
100
101
  }
101
102
 
102
103
  function caseMemorySeedsPath(): string {
103
- return resolve(import.meta.dir, "../eval/continuity/case-seeds.json");
104
+ return resolve(metaDir(import.meta), "../eval/continuity/case-seeds.json");
104
105
  }
105
106
 
106
107
  function memorySeedsPath(): string {
107
- return resolve(import.meta.dir, "../eval/continuity/memory-seeds.json");
108
+ return resolve(metaDir(import.meta), "../eval/continuity/memory-seeds.json");
108
109
  }
109
110
 
110
111
  function parseMetadata(raw?: string): Record<string, unknown> {
@@ -295,16 +296,13 @@ export function assessContinuityBaseline(
295
296
  export async function runDoctor(options: { ci?: boolean } = {}): Promise<CheckResult[]> {
296
297
  const results: CheckResult[] = [];
297
298
 
298
- // 1. Bun runtime
299
+ // 1. Runtime check
299
300
  const bunVersion = typeof Bun !== "undefined" ? Bun.version : null;
300
301
  if (bunVersion) {
301
- results.push(pass("Bun runtime", `v${bunVersion}`));
302
+ results.push(pass("Runtime", `Bun v${bunVersion}`));
302
303
  } else {
303
- results.push(fail(
304
- "Bun runtime",
305
- "Bun not detected (running under Node?)",
306
- "Install Bun: curl -fsSL https://bun.sh/install | bash"
307
- ));
304
+ const nodeVersion = process.version;
305
+ results.push(pass("Runtime", `Node.js ${nodeVersion} (Bun recommended for best performance)`));
308
306
  }
309
307
 
310
308
  // 2. Config file
@@ -0,0 +1,227 @@
1
+ /**
2
+ * AD-1: Unified Dream Pipeline
3
+ *
4
+ * Inspired by Claude Code's Auto Dream — a four-phase consolidation pipeline
5
+ * that orchestrates existing RecallNest components into a coherent "dream" cycle.
6
+ *
7
+ * Phases:
8
+ * 1. Orient — scan memory state, get latest checkpoint, assess activity
9
+ * 2. Gather — collect recent signals (new writes since last dream)
10
+ * 3. Consolidate — cluster, merge, extract patterns, generate insights
11
+ * 4. Prune — archive low-value memories, enforce storage hygiene
12
+ *
13
+ * Unlike Auto Dream's grep-only approach, RecallNest uses vector search +
14
+ * LLM-driven consolidation for semantic-level maintenance.
15
+ */
16
+
17
+ import type { MemoryStore } from "./store.js";
18
+ import type { LLMClient } from "./llm-client.js";
19
+ import type { Embedder } from "./embedder.js";
20
+ import { ConsolidationEngine, clusterAndConsolidate, DEFAULT_CONSOLIDATION_CONFIG } from "./consolidation-engine.js";
21
+ import { maybeRunGc, type GcResult, type AutoGcConfig, DEFAULT_AUTO_GC_CONFIG } from "./auto-gc.js";
22
+ import { getWriteCount, resetWriteCount } from "./activity-counter.js";
23
+ import { isActiveMemory } from "./memory-evolution.js";
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Types
27
+ // ---------------------------------------------------------------------------
28
+
29
+ export interface DreamConfig {
30
+ /** Minimum writes since last dream to justify running (default: 10) */
31
+ minWritesForDream: number;
32
+ /** Consolidation cluster threshold (default: 0.82) */
33
+ clusterThreshold: number;
34
+ /** Minimum cluster size for insight/pattern generation (default: 3) */
35
+ minClusterSize: number;
36
+ /** Enable cross-memory pattern extraction in consolidation (default: true) */
37
+ extractPatterns: boolean;
38
+ /** Max entries to scan per consolidation run (default: 500) */
39
+ maxEntriesPerRun: number;
40
+ /** GC config for prune phase */
41
+ gc: AutoGcConfig;
42
+ }
43
+
44
+ export const DEFAULT_DREAM_CONFIG: DreamConfig = {
45
+ minWritesForDream: 10,
46
+ clusterThreshold: 0.82,
47
+ minClusterSize: 3,
48
+ extractPatterns: true,
49
+ maxEntriesPerRun: 500,
50
+ gc: DEFAULT_AUTO_GC_CONFIG,
51
+ };
52
+
53
+ export interface DreamPhaseResult {
54
+ phase: "orient" | "gather" | "consolidate" | "prune";
55
+ detail: string;
56
+ }
57
+
58
+ export interface DreamResult {
59
+ ran: boolean;
60
+ reason?: string;
61
+ phases: DreamPhaseResult[];
62
+ stats: {
63
+ totalMemories: number;
64
+ activeMemories: number;
65
+ writesSinceLastDream: number;
66
+ clustersFound: number;
67
+ insightsGenerated: number;
68
+ patternsExtracted: number;
69
+ mergedCount: number;
70
+ archivedCount: number;
71
+ };
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Pipeline
76
+ // ---------------------------------------------------------------------------
77
+
78
+ export async function runDream(params: {
79
+ store: MemoryStore;
80
+ llm: LLMClient | null;
81
+ embedder: Pick<Embedder, "embedPassage">;
82
+ scope: string;
83
+ config?: Partial<DreamConfig>;
84
+ /** Skip the minimum-writes gate (force run) */
85
+ force?: boolean;
86
+ }): Promise<DreamResult> {
87
+ const { store, llm, embedder, scope, force = false } = params;
88
+ const config = { ...DEFAULT_DREAM_CONFIG, ...params.config };
89
+ const phases: DreamPhaseResult[] = [];
90
+
91
+ const stats = {
92
+ totalMemories: 0,
93
+ activeMemories: 0,
94
+ writesSinceLastDream: 0,
95
+ clustersFound: 0,
96
+ insightsGenerated: 0,
97
+ patternsExtracted: 0,
98
+ mergedCount: 0,
99
+ archivedCount: 0,
100
+ };
101
+
102
+ // =========================================================================
103
+ // Phase 1: Orient — assess current memory state
104
+ // =========================================================================
105
+ const writeCount = getWriteCount();
106
+ stats.writesSinceLastDream = writeCount;
107
+
108
+ const storeStats = await store.stats([scope]);
109
+ stats.totalMemories = storeStats.totalCount ?? 0;
110
+
111
+ if (!force && writeCount < config.minWritesForDream) {
112
+ return {
113
+ ran: false,
114
+ reason: `insufficient_writes (${writeCount}/${config.minWritesForDream})`,
115
+ phases: [{
116
+ phase: "orient",
117
+ detail: `${stats.totalMemories} memories, ${writeCount} writes since last dream — below threshold`,
118
+ }],
119
+ stats,
120
+ };
121
+ }
122
+
123
+ phases.push({
124
+ phase: "orient",
125
+ detail: `${stats.totalMemories} memories, ${writeCount} writes since last dream`,
126
+ });
127
+
128
+ // =========================================================================
129
+ // Phase 2: Gather — collect active entries for consolidation
130
+ // =========================================================================
131
+ const entries = await store.list([scope], undefined, config.maxEntriesPerRun, 0);
132
+ const active = entries.filter(e => isActiveMemory(e.metadata));
133
+ stats.activeMemories = active.length;
134
+
135
+ phases.push({
136
+ phase: "gather",
137
+ detail: `${active.length} active entries gathered from ${entries.length} total`,
138
+ });
139
+
140
+ if (active.length < config.minClusterSize) {
141
+ phases.push({ phase: "consolidate", detail: "skipped — too few active entries" });
142
+ phases.push({ phase: "prune", detail: "skipped — too few entries for GC" });
143
+ resetWriteCount();
144
+ return { ran: true, reason: "completed_early", phases, stats };
145
+ }
146
+
147
+ // =========================================================================
148
+ // Phase 3: Consolidate — cluster, merge, generate insights + patterns
149
+ // =========================================================================
150
+
151
+ // 3a: Deterministic consolidation (merge near-duplicates, link clusters)
152
+ const engine = new ConsolidationEngine(store, {
153
+ ...DEFAULT_CONSOLIDATION_CONFIG,
154
+ clusterThreshold: config.clusterThreshold,
155
+ maxEntriesPerRun: config.maxEntriesPerRun,
156
+ });
157
+ const consolidation = await engine.run(scope);
158
+ stats.clustersFound += consolidation.clustersFound;
159
+ stats.mergedCount += consolidation.mergedCount;
160
+
161
+ // 3b: LLM-driven cluster consolidation (insights + patterns) — requires LLM
162
+ if (llm) {
163
+ const clusterResult = await clusterAndConsolidate({
164
+ entries: active,
165
+ embedder,
166
+ llm,
167
+ store,
168
+ scope,
169
+ minClusterSize: config.minClusterSize,
170
+ clusterThreshold: config.clusterThreshold - 0.07, // slightly lower for semantic clustering
171
+ extractPatterns: config.extractPatterns,
172
+ });
173
+ stats.clustersFound += clusterResult.clustersFound;
174
+ stats.insightsGenerated = clusterResult.insightsGenerated;
175
+ stats.patternsExtracted = clusterResult.patternsExtracted;
176
+ }
177
+
178
+ phases.push({
179
+ phase: "consolidate",
180
+ detail: `${stats.clustersFound} clusters, ${stats.mergedCount} merged, ${stats.insightsGenerated} insights, ${stats.patternsExtracted} patterns`,
181
+ });
182
+
183
+ // =========================================================================
184
+ // Phase 4: Prune — archive low-value memories
185
+ // =========================================================================
186
+ const gcResult = await maybeRunGc(store, config.gc);
187
+ stats.archivedCount = gcResult.archivedCount;
188
+
189
+ phases.push({
190
+ phase: "prune",
191
+ detail: gcResult.triggered
192
+ ? `${gcResult.archivedCount} entries archived`
193
+ : `skipped — ${gcResult.reason}`,
194
+ });
195
+
196
+ // Reset activity counter after successful dream
197
+ resetWriteCount();
198
+
199
+ return { ran: true, phases, stats };
200
+ }
201
+
202
+ // ---------------------------------------------------------------------------
203
+ // Formatting
204
+ // ---------------------------------------------------------------------------
205
+
206
+ export function formatDreamResult(result: DreamResult): string {
207
+ if (!result.ran) {
208
+ return `Dream skipped: ${result.reason}\n${result.phases[0]?.detail ?? ""}`;
209
+ }
210
+
211
+ const lines = [
212
+ "Dream completed",
213
+ "",
214
+ ...result.phases.map(p => `[${p.phase}] ${p.detail}`),
215
+ "",
216
+ "Stats:",
217
+ ` Total memories: ${result.stats.totalMemories}`,
218
+ ` Active: ${result.stats.activeMemories}`,
219
+ ` Clusters: ${result.stats.clustersFound}`,
220
+ ` Insights: ${result.stats.insightsGenerated}`,
221
+ ` Patterns: ${result.stats.patternsExtracted}`,
222
+ ` Merged: ${result.stats.mergedCount}`,
223
+ ` Archived: ${result.stats.archivedCount}`,
224
+ ];
225
+
226
+ return lines.join("\n");
227
+ }
@@ -0,0 +1,90 @@
1
+ import type { EmotionMetadata } from "./memory-schema.js";
2
+ import { isEmotionScoringEnabled } from "./memory-schema.js";
3
+
4
+ const NEGATIVE_SIGNALS: string[] = [
5
+ // English
6
+ "fail", "failed", "failure", "broken", "bug", "error", "wrong", "crash",
7
+ "frustrat", "hate", "terrible", "awful", "annoying", "pain", "stuck",
8
+ "problem", "issue", "mess", "ugly", "worst",
9
+ // Chinese
10
+ "失败", "痛苦", "困扰", "崩溃", "报错", "出错", "难受", "烦",
11
+ "卡住", "折腾", "头疼", "坑", "讨厌", "不喜欢", "糟糕", "恶心",
12
+ ];
13
+
14
+ const POSITIVE_SIGNALS: string[] = [
15
+ // English
16
+ "solved", "fixed", "works", "perfect", "great", "love", "excellent",
17
+ "success", "breakthrough", "finally", "awesome", "beautiful", "clean",
18
+ "elegant", "smooth", "done", "shipped",
19
+ // Chinese
20
+ "搞定", "成功", "突破", "完美", "太好了", "顺利", "解决", "漂亮",
21
+ "优雅", "通过", "上线", "喜欢", "开心", "厉害",
22
+ ];
23
+
24
+ const HIGH_AROUSAL_SIGNALS: string[] = [
25
+ "!", "!!", "urgent", "critical", "immediately", "ASAP", "emergency",
26
+ "紧急", "立刻", "马上", "赶紧", "救命",
27
+ ];
28
+
29
+ /** Chinese negation prefixes that flip positive → negative */
30
+ const CN_NEGATION_PREFIXES = ["不", "没", "非", "无", "别", "未"];
31
+
32
+ function clamp(value: number, min: number, max: number): number {
33
+ return Math.max(min, Math.min(max, value));
34
+ }
35
+
36
+ /**
37
+ * Count positive signal matches, handling Chinese negation.
38
+ * "不喜欢" → skip positive, add to negatedCount (caller adds to negative).
39
+ * Returns { positive, negated } counts.
40
+ */
41
+ function countPositiveSignals(lower: string): { positive: number; negated: number } {
42
+ let positive = 0;
43
+ let negated = 0;
44
+ for (const signal of POSITIVE_SIGNALS) {
45
+ const s = signal.toLowerCase();
46
+ const idx = lower.indexOf(s);
47
+ if (idx === -1) continue;
48
+ const isNegated = idx > 0 && CN_NEGATION_PREFIXES.some(neg => lower.substring(idx - neg.length, idx) === neg);
49
+ if (isNegated) {
50
+ negated++;
51
+ } else {
52
+ positive++;
53
+ }
54
+ }
55
+ return { positive, negated };
56
+ }
57
+
58
+ /**
59
+ * Detect emotional valence and arousal from text using keyword heuristics.
60
+ * Zero LLM cost. Returns neutral emotion for empty text.
61
+ */
62
+ export function detectEmotion(text: string): EmotionMetadata {
63
+ if (!text || text.length === 0) {
64
+ return { valence: 0, arousal: 0, label: "neutral", salience: 0, source: "keyword" };
65
+ }
66
+
67
+ const lower = text.toLowerCase();
68
+
69
+ const negCount = NEGATIVE_SIGNALS.filter(s => lower.includes(s.toLowerCase())).length;
70
+ const { positive: posCount, negated: negatedPosCount } = countPositiveSignals(lower);
71
+ const arousalCount = HIGH_AROUSAL_SIGNALS.filter(s => text.includes(s)).length;
72
+
73
+ // Negated positive signals (e.g. "不开心") count as additional negatives
74
+ const valence = clamp((posCount - negCount - negatedPosCount) * 0.3, -1, 1);
75
+ const arousal = clamp(arousalCount * 0.25, 0, 1);
76
+ const label = valence > 0.25 ? "positive" : valence < -0.25 ? "negative" : "neutral";
77
+
78
+ // Salience: composite mnemonic significance — average of emotional intensity and arousal
79
+ const salience = clamp((Math.abs(valence) + arousal) / 2, 0, 1);
80
+
81
+ return { valence, arousal, label, salience, source: "keyword" };
82
+ }
83
+
84
+ /**
85
+ * Conditionally detect emotion. Returns null when feature flag is off.
86
+ */
87
+ export function detectEmotionIfEnabled(text: string): EmotionMetadata | null {
88
+ if (!isEmotionScoringEnabled()) return null;
89
+ return detectEmotion(text);
90
+ }
package/src/eval.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
4
  import { dirname, resolve } from "node:path";
5
5
 
6
+ import { metaDir } from "./compat.js";
6
7
  import { buildSessionCheckpointRecord, normalizeCheckpointScope } from "./session-engine.js";
7
8
  import type { ResumeContextResponse, SessionCheckpointRecord } from "./session-schema.js";
8
9
  import { composeResumeContext } from "./context-composer.js";
@@ -158,8 +159,8 @@ function parseArgs(args: string[]): EvalArgs {
158
159
 
159
160
  function defaultCasesPath(mode: EvalMode): string {
160
161
  return mode === "continuity"
161
- ? resolve(import.meta.dir, "../eval/continuity/cases.json")
162
- : resolve(import.meta.dir, "../eval/cases.json");
162
+ ? resolve(metaDir(import.meta), "../eval/continuity/cases.json")
163
+ : resolve(metaDir(import.meta), "../eval/cases.json");
163
164
  }
164
165
 
165
166
  function loadCases<T>(mode: EvalMode, pathArg?: string): T[] {