recallnest 1.4.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 (261) hide show
  1. package/.claude-plugin/CLAUDE.md +29 -0
  2. package/.claude-plugin/marketplace.json +18 -0
  3. package/.env.example +3 -0
  4. package/.github/workflows/ci.yml +28 -0
  5. package/.mcp.json +9 -0
  6. package/CHANGELOG.md +51 -0
  7. package/CLAUDE.md +42 -0
  8. package/DEMO.md +94 -0
  9. package/EVAL.md +89 -0
  10. package/FAILURES.md +75 -0
  11. package/LICENSE +21 -0
  12. package/OPERATIONS.md +59 -0
  13. package/README.md +427 -0
  14. package/README_CN.md +425 -0
  15. package/ROADMAP.md +198 -0
  16. package/assets/recallnest-workbench.png +0 -0
  17. package/assets/ui/app.js +807 -0
  18. package/assets/ui/index.html +154 -0
  19. package/assets/ui/styles.css +605 -0
  20. package/assets/wechat-qr.jpg +0 -0
  21. package/config.json.example +38 -0
  22. package/daily-recall.sh +87 -0
  23. package/daily-reflections/2026-03-05.md +15 -0
  24. package/daily-reflections/2026-03-06.md +15 -0
  25. package/daily-reflections/2026-03-07.md +7 -0
  26. package/daily-reflections/2026-03-08.md +34 -0
  27. package/daily-reflections/2026-03-09.md +35 -0
  28. package/daily-reflections/2026-03-10.md +36 -0
  29. package/daily-reflections/2026-03-11.md +35 -0
  30. package/daily-reflections/2026-03-12.md +36 -0
  31. package/daily-reflections/2026-03-13.md +35 -0
  32. package/daily-reflections/2026-03-14.md +35 -0
  33. package/daily-reflections/2026-03-15.md +35 -0
  34. package/daily-reflections/2026-03-16.md +41 -0
  35. package/daily-reflections/2026-03-17.md +10 -0
  36. package/daily-reflections/2026-03-18.md +10 -0
  37. package/daily-reflections/2026-03-19.md +10 -0
  38. package/daily-reflections/2026-03-20.md +10 -0
  39. package/daily-reflections/2026-03-21.md +10 -0
  40. package/daily-reflections/2026-03-22.md +10 -0
  41. package/daily-reflections/2026-03-23.md +10 -0
  42. package/daily-reflections/2026-03-24.md +10 -0
  43. package/daily-reflections/2026-03-25.md +32 -0
  44. package/daily-reflections/2026-03-26.md +37 -0
  45. package/daily-reflections/2026-03-27.md +35 -0
  46. package/daily-reflections/2026-03-28.md +35 -0
  47. package/daily-reflections/2026-03-29.md +35 -0
  48. package/daily-reflections/2026-03-30.md +36 -0
  49. package/daily-reflections/2026-03-31.md +35 -0
  50. package/daily-reflections/2026-04-01.md +35 -0
  51. package/docs/api-reference.md +1291 -0
  52. package/docs/architecture.md +168 -0
  53. package/docs/memory-boundary-contract.md +226 -0
  54. package/docs/memory-categories.md +119 -0
  55. package/docs/pipeline-audit-ultramemory.md +177 -0
  56. package/docs/plans/2026-03-08-query-expansion.md +270 -0
  57. package/docs/plans/2026-03-16-continuity-layer.md +196 -0
  58. package/docs/plans/mcp-tiering-plan.md +126 -0
  59. package/docs/research/recall-quality-audit-2026-03-22.md +153 -0
  60. package/docs/self-evolution.md +198 -0
  61. package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +782 -0
  62. package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +77 -0
  63. package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +95 -0
  64. package/eval/cases.json +85 -0
  65. package/eval/continuity/case-seeds.json +90 -0
  66. package/eval/continuity/cases.json +302 -0
  67. package/eval/continuity/memory-seeds.json +26 -0
  68. package/eval/continuity/pattern-seeds.json +100 -0
  69. package/eval/continuity/results-2026-03-16.md +117 -0
  70. package/eval/continuity/results-2026-03-19.md +453 -0
  71. package/eval/continuity/runbook.md +110 -0
  72. package/eval/continuity/seed-cases.md +54 -0
  73. package/eval/reports/2026-03-06-abstract-query-baseline.md +96 -0
  74. package/eval/reports/2026-03-06-aws-ssh-baseline.md +82 -0
  75. package/eval/reports/2026-03-06-baseline.md +82 -0
  76. package/eval/reports/query-expansion.md +138 -0
  77. package/integrations/README.md +57 -0
  78. package/integrations/claude-code/README.md +133 -0
  79. package/integrations/claude-code/claude-md-snippet.md +31 -0
  80. package/integrations/claude-code/setup.sh +66 -0
  81. package/integrations/claude-code/smoke-continuity.sh +452 -0
  82. package/integrations/codex/README.md +49 -0
  83. package/integrations/codex/agents-md-snippet.md +31 -0
  84. package/integrations/codex/setup.sh +58 -0
  85. package/integrations/examples/claude-agent-sdk/memory-agent.ts +182 -0
  86. package/integrations/examples/langchain/memory-chain.py +111 -0
  87. package/integrations/examples/openai-agents-sdk/memory-agent.py +109 -0
  88. package/integrations/gemini-cli/README.md +57 -0
  89. package/integrations/gemini-cli/gemini-md-snippet.md +31 -0
  90. package/integrations/gemini-cli/setup.sh +64 -0
  91. package/integrations/shared/install-managed-block.sh +51 -0
  92. package/lm +19 -0
  93. package/package.json +50 -0
  94. package/plan-v1.2-distributable.md +105 -0
  95. package/scripts/archive-distilled-junk.ts +116 -0
  96. package/scripts/backfill-l0l1l2.ts +98 -0
  97. package/scripts/blind-scan.ts +588 -0
  98. package/scripts/cleanup-step1-exact-dups.ts +117 -0
  99. package/scripts/cleanup-step2-template-dups.ts +113 -0
  100. package/scripts/cleanup-step3-stale-content.ts +149 -0
  101. package/scripts/cleanup-step4-orphan-scopes.ts +131 -0
  102. package/scripts/delete-distilled-junk.ts +88 -0
  103. package/scripts/distill-facts.ts +305 -0
  104. package/scripts/health-check.ts +343 -0
  105. package/scripts/incremental-ingest.sh +71 -0
  106. package/scripts/mlp-health-check.ts +382 -0
  107. package/scripts/pull-claude-desktop.ts +515 -0
  108. package/scripts/re-ingest-cc.sh +66 -0
  109. package/scripts/rebalance-memory-health.ts +181 -0
  110. package/scripts/run-desktop-ingest.sh +11 -0
  111. package/scripts/start-server.sh +12 -0
  112. package/scripts/viz-dashboard.html +423 -0
  113. package/scripts/viz-export.ts +703 -0
  114. package/src/__tests__/access-tracker-novelty.test.ts +228 -0
  115. package/src/__tests__/auto-consolidation.test.ts +168 -0
  116. package/src/__tests__/auto-recall.test.ts +131 -0
  117. package/src/__tests__/batch-dedup.test.ts +65 -0
  118. package/src/__tests__/capture-engine.test.ts +1142 -0
  119. package/src/__tests__/capture-heuristic.test.ts +171 -0
  120. package/src/__tests__/cascade-forget.test.ts +210 -0
  121. package/src/__tests__/category-min-scores.test.ts +33 -0
  122. package/src/__tests__/confidence-tracker.test.ts +196 -0
  123. package/src/__tests__/conflict-advisor.test.ts +206 -0
  124. package/src/__tests__/conflict-escalation.test.ts +149 -0
  125. package/src/__tests__/conflict-lifecycle.test.ts +83 -0
  126. package/src/__tests__/conflict-output.test.ts +85 -0
  127. package/src/__tests__/conflict-store.test.ts +374 -0
  128. package/src/__tests__/consolidation-engine.test.ts +181 -0
  129. package/src/__tests__/context-composer-pins.test.ts +64 -0
  130. package/src/__tests__/context-composer-task-selection.test.ts +24 -0
  131. package/src/__tests__/context-renderer.test.ts +91 -0
  132. package/src/__tests__/continuity-eval.test.ts +275 -0
  133. package/src/__tests__/continuity-schema.test.ts +97 -0
  134. package/src/__tests__/core-recall-eval.test.ts +235 -0
  135. package/src/__tests__/core-summary.test.ts +109 -0
  136. package/src/__tests__/dedup-actions.test.ts +58 -0
  137. package/src/__tests__/deterministic-id.test.ts +39 -0
  138. package/src/__tests__/doctor.test.ts +303 -0
  139. package/src/__tests__/drill-down.test.ts +108 -0
  140. package/src/__tests__/embedder.test.ts +122 -0
  141. package/src/__tests__/entity-resolver.test.ts +58 -0
  142. package/src/__tests__/event-segmenter.test.ts +153 -0
  143. package/src/__tests__/hotness-blend.test.ts +76 -0
  144. package/src/__tests__/ingest.test.ts +360 -0
  145. package/src/__tests__/kg-extractor.test.ts +184 -0
  146. package/src/__tests__/kg-store.test.ts +198 -0
  147. package/src/__tests__/llm-consolidation.test.ts +226 -0
  148. package/src/__tests__/memory-boundaries.test.ts +237 -0
  149. package/src/__tests__/memory-health-rebalance.test.ts +144 -0
  150. package/src/__tests__/memory-output.test.ts +215 -0
  151. package/src/__tests__/multi-vector.test.ts +242 -0
  152. package/src/__tests__/ppr-traversal.test.ts +256 -0
  153. package/src/__tests__/preference-guard-smoke.test.ts +211 -0
  154. package/src/__tests__/preference-matcher.test.ts +230 -0
  155. package/src/__tests__/preference-slots.test.ts +106 -0
  156. package/src/__tests__/prospective-memory.test.ts +283 -0
  157. package/src/__tests__/query-entity-detector.test.ts +171 -0
  158. package/src/__tests__/query-expander.test.ts +34 -0
  159. package/src/__tests__/recall-mode.test.ts +64 -0
  160. package/src/__tests__/result-synthesizer.test.ts +198 -0
  161. package/src/__tests__/resume-context.test.ts +4743 -0
  162. package/src/__tests__/retrieval-trace.test.ts +213 -0
  163. package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
  164. package/src/__tests__/retroactive-boost.test.ts +223 -0
  165. package/src/__tests__/scope-inventory.test.ts +200 -0
  166. package/src/__tests__/scope-policy.test.ts +69 -0
  167. package/src/__tests__/session-checkpoint.test.ts +259 -0
  168. package/src/__tests__/session-gc.test.ts +215 -0
  169. package/src/__tests__/store-scope-audit.test.ts +81 -0
  170. package/src/__tests__/temporal-parser.test.ts +241 -0
  171. package/src/__tests__/term-registry.test.ts +121 -0
  172. package/src/__tests__/tool-output-compressor.test.ts +133 -0
  173. package/src/__tests__/version-manager.test.ts +213 -0
  174. package/src/__tests__/workflow-observation.test.ts +400 -0
  175. package/src/access-tracker.ts +316 -0
  176. package/src/adaptive-retrieval.ts +64 -0
  177. package/src/api-server.ts +667 -0
  178. package/src/asset-sync.ts +79 -0
  179. package/src/auto-consolidation.ts +133 -0
  180. package/src/auto-gc.ts +124 -0
  181. package/src/auto-recall.ts +95 -0
  182. package/src/capture-engine.ts +931 -0
  183. package/src/capture-heuristic.ts +177 -0
  184. package/src/cascade-forget.ts +135 -0
  185. package/src/chunker.ts +253 -0
  186. package/src/cli.ts +1917 -0
  187. package/src/confidence-tracker.ts +166 -0
  188. package/src/conflict-advisor.ts +433 -0
  189. package/src/conflict-engine.ts +181 -0
  190. package/src/conflict-escalation.ts +138 -0
  191. package/src/conflict-lifecycle.ts +67 -0
  192. package/src/conflict-output.ts +218 -0
  193. package/src/conflict-schema.ts +130 -0
  194. package/src/conflict-store.ts +127 -0
  195. package/src/consolidation-engine.ts +256 -0
  196. package/src/context-composer-pins.ts +183 -0
  197. package/src/context-composer-queries.ts +121 -0
  198. package/src/context-composer-scope.ts +56 -0
  199. package/src/context-composer-stable-selection.ts +200 -0
  200. package/src/context-composer-stable.ts +217 -0
  201. package/src/context-composer-task-fallbacks.ts +36 -0
  202. package/src/context-composer-task-ranking.ts +197 -0
  203. package/src/context-composer-task-results.ts +137 -0
  204. package/src/context-composer-task-selection.ts +342 -0
  205. package/src/context-composer-text.ts +57 -0
  206. package/src/context-composer.ts +300 -0
  207. package/src/context-renderer.ts +179 -0
  208. package/src/decay-engine.ts +189 -0
  209. package/src/doctor.ts +543 -0
  210. package/src/embedder.ts +557 -0
  211. package/src/entity-resolver.ts +131 -0
  212. package/src/eval.ts +632 -0
  213. package/src/event-segmenter.ts +216 -0
  214. package/src/ingest.ts +1822 -0
  215. package/src/kg-extractor.ts +217 -0
  216. package/src/kg-store.ts +353 -0
  217. package/src/legacy-scope-review.ts +53 -0
  218. package/src/llm-client.ts +629 -0
  219. package/src/llm-consolidation.ts +168 -0
  220. package/src/mcp-server.ts +1337 -0
  221. package/src/memory-assets.ts +475 -0
  222. package/src/memory-boundaries.ts +354 -0
  223. package/src/memory-health-rebalance.ts +220 -0
  224. package/src/memory-output.ts +474 -0
  225. package/src/memory-schema.ts +184 -0
  226. package/src/multi-vector.ts +234 -0
  227. package/src/noise-filter.ts +155 -0
  228. package/src/ppr-traversal.ts +279 -0
  229. package/src/preference-matcher.ts +174 -0
  230. package/src/preference-slots.ts +259 -0
  231. package/src/prospective-memory.ts +198 -0
  232. package/src/query-entity-detector.ts +207 -0
  233. package/src/query-expander.ts +98 -0
  234. package/src/result-synthesizer.ts +114 -0
  235. package/src/retrieval-profiles.ts +101 -0
  236. package/src/retrieval-trace.ts +94 -0
  237. package/src/retriever.ts +1338 -0
  238. package/src/retroactive-boost.ts +154 -0
  239. package/src/rif.ts +73 -0
  240. package/src/runtime-config.ts +172 -0
  241. package/src/schema-utils.ts +56 -0
  242. package/src/scope-inventory.ts +310 -0
  243. package/src/scope-policy.ts +142 -0
  244. package/src/session-engine.ts +168 -0
  245. package/src/session-output.ts +96 -0
  246. package/src/session-schema.ts +60 -0
  247. package/src/session-store.ts +293 -0
  248. package/src/stderr-log.ts +16 -0
  249. package/src/store.ts +903 -0
  250. package/src/temporal-parser.ts +318 -0
  251. package/src/term-registry.ts +791 -0
  252. package/src/tool-output-compressor.ts +115 -0
  253. package/src/tracker.ts +57 -0
  254. package/src/ui-server.ts +430 -0
  255. package/src/version-manager.ts +172 -0
  256. package/src/workflow-observation-engine.ts +316 -0
  257. package/src/workflow-observation-managed.ts +83 -0
  258. package/src/workflow-observation-output.ts +86 -0
  259. package/src/workflow-observation-schema.ts +87 -0
  260. package/src/workflow-observation-scope-review.ts +409 -0
  261. package/src/workflow-observation-store.ts +69 -0
@@ -0,0 +1,79 @@
1
+ import type { Embedder } from "./embedder.js";
2
+ import type { MemoryStore } from "./store.js";
3
+ import type { BriefAsset, MemoryAsset, PinAsset } from "./memory-assets.js";
4
+
5
+ function buildPinnedAssetText(asset: PinAsset): string {
6
+ const tags = asset.tags.join(", ");
7
+ return [
8
+ `[Pinned Asset] ${asset.title}`,
9
+ `Summary: ${asset.summary}`,
10
+ `Snippet: ${asset.snippet}`,
11
+ `Original Scope: ${asset.source.scope}`,
12
+ tags ? `Tags: ${tags}` : "",
13
+ ].filter(Boolean).join("\n");
14
+ }
15
+
16
+ function buildBriefAssetText(asset: BriefAsset): string {
17
+ const tags = asset.tags.join(", ");
18
+ return [
19
+ `[Memory Brief] ${asset.title}`,
20
+ `Query: ${asset.query}`,
21
+ `Profile: ${asset.profile}`,
22
+ `Summary: ${asset.summary}`,
23
+ asset.takeaways.length > 0 ? `Takeaways: ${asset.takeaways.join(" | ")}` : "",
24
+ asset.reusableCandidates.length > 0 ? `Reusable: ${asset.reusableCandidates.join(" | ")}` : "",
25
+ asset.sources.length > 0 ? `Sources: ${asset.sources.map((item) => `${item.source}(${item.hits})`).join(", ")}` : "",
26
+ tags ? `Tags: ${tags}` : "",
27
+ ].filter(Boolean).join("\n");
28
+ }
29
+
30
+ function buildAssetText(asset: MemoryAsset): string {
31
+ return asset.type === "memory-brief"
32
+ ? buildBriefAssetText(asset)
33
+ : buildPinnedAssetText(asset);
34
+ }
35
+
36
+ function assetImportance(asset: MemoryAsset): number {
37
+ return asset.type === "memory-brief" ? 0.72 : 0.96;
38
+ }
39
+
40
+ export async function indexAsset(
41
+ store: MemoryStore,
42
+ embedder: Embedder,
43
+ asset: MemoryAsset,
44
+ ): Promise<void> {
45
+ const text = buildAssetText(asset);
46
+ const vector = await embedder.embedPassage(text);
47
+ const metadata = asset.type === "memory-brief"
48
+ ? {
49
+ source: "asset",
50
+ assetId: asset.id,
51
+ assetType: asset.type,
52
+ title: asset.title,
53
+ query: asset.query,
54
+ profile: asset.profile,
55
+ tags: asset.tags,
56
+ sources: asset.sources.map((item) => item.source),
57
+ }
58
+ : {
59
+ source: "asset",
60
+ assetId: asset.id,
61
+ assetType: asset.type,
62
+ title: asset.title,
63
+ originalMemoryId: asset.source.memoryId,
64
+ originalScope: asset.source.scope,
65
+ tags: asset.tags,
66
+ };
67
+ await store.store({
68
+ text,
69
+ vector,
70
+ category: "decision",
71
+ scope: asset.type === "memory-brief"
72
+ ? `asset:brief:${asset.id.slice(0, 8)}`
73
+ : `asset:${asset.id.slice(0, 8)}`,
74
+ importance: assetImportance(asset),
75
+ metadata: JSON.stringify(metadata),
76
+ });
77
+ }
78
+
79
+ export const indexPinnedAsset = indexAsset;
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Auto Consolidation — Condition-driven consolidation trigger.
3
+ *
4
+ * Agent-First reasoning: manual consolidation is unsustainable. Trigger
5
+ * automatically when enough new memories have accumulated AND enough time
6
+ * has passed since the last run. Same dual-gate pattern as auto-gc.ts.
7
+ *
8
+ * Brain-science label: "NREM triple-coupling" — multiple conditions must
9
+ * align before consolidation fires. The label is for branding; the real
10
+ * reason is operational hygiene.
11
+ */
12
+
13
+ import type { MemoryStore } from "./store.js";
14
+ import { ConsolidationEngine, type ConsolidationConfig, type ConsolidationResult, DEFAULT_CONSOLIDATION_CONFIG } from "./consolidation-engine.js";
15
+ import type { LLMClient } from "./llm-client.js";
16
+ import { isLLMConsolidationEnabled, evaluateCluster, executeMergeDecisions } from "./llm-consolidation.js";
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Config
20
+ // ---------------------------------------------------------------------------
21
+
22
+ export interface AutoConsolidationConfig {
23
+ /** Minimum new memories since last consolidation before triggering (default: 50) */
24
+ minNewMemories: number;
25
+ /** Minimum hours since last consolidation (default: 12) */
26
+ minHoursSinceLastRun: number;
27
+ /** Consolidation engine config (cluster/merge thresholds) */
28
+ consolidation: ConsolidationConfig;
29
+ }
30
+
31
+ export const DEFAULT_AUTO_CONSOLIDATION_CONFIG: AutoConsolidationConfig = {
32
+ minNewMemories: 50,
33
+ minHoursSinceLastRun: 12,
34
+ consolidation: DEFAULT_CONSOLIDATION_CONFIG,
35
+ };
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // State
39
+ // ---------------------------------------------------------------------------
40
+
41
+ let lastRunTimestamp = 0;
42
+ let lastRunMemoryCount = 0;
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Core
46
+ // ---------------------------------------------------------------------------
47
+
48
+ export interface AutoConsolidationResult {
49
+ triggered: boolean;
50
+ reason?: string;
51
+ consolidation?: ConsolidationResult;
52
+ }
53
+
54
+ /**
55
+ * Check conditions and run consolidation if thresholds are met.
56
+ * Returns immediately if conditions not met (no-op).
57
+ */
58
+ export async function maybeConsolidate(
59
+ store: MemoryStore,
60
+ scope: string,
61
+ config: AutoConsolidationConfig = DEFAULT_AUTO_CONSOLIDATION_CONFIG,
62
+ /** Tier 3.7: Optional LLM client for semantic consolidation decisions */
63
+ llm?: LLMClient | null,
64
+ ): Promise<AutoConsolidationResult> {
65
+ const stats = await store.stats([scope]);
66
+ const currentCount = stats.total ?? 0;
67
+
68
+ // Condition 1: enough new memories since last run
69
+ const newSinceLastRun = currentCount - lastRunMemoryCount;
70
+ if (newSinceLastRun < config.minNewMemories) {
71
+ return { triggered: false, reason: "insufficient_new_memories" };
72
+ }
73
+
74
+ // Condition 2: enough time since last run
75
+ const hoursSinceLastRun = (Date.now() - lastRunTimestamp) / 3_600_000;
76
+ if (hoursSinceLastRun < config.minHoursSinceLastRun) {
77
+ return { triggered: false, reason: "too_soon" };
78
+ }
79
+
80
+ // Both conditions met — run consolidation
81
+ lastRunTimestamp = Date.now();
82
+ lastRunMemoryCount = currentCount;
83
+
84
+ const engine = new ConsolidationEngine(store, config.consolidation);
85
+ const result = await engine.run(scope);
86
+
87
+ // Tier 3.7: LLM post-processing of linked clusters.
88
+ // When enabled, LLM evaluates "clustered_with" relations and may
89
+ // upgrade them to version groups (merges) based on semantic judgment.
90
+ if (isLLMConsolidationEnabled() && llm && result.relationsAdded > 0) {
91
+ let llmMerges = 0;
92
+ // Find entries with clustered_with that aren't already in version groups
93
+ const allEntries = await store.list([scope], undefined, config.consolidation.maxEntriesPerRun ?? 500);
94
+ const clusterMap = new Map<string, MemoryEntry[]>();
95
+
96
+ for (const entry of allEntries) {
97
+ try {
98
+ const meta = JSON.parse(entry.metadata || "{}");
99
+ const members = meta.cluster_members as string[] | undefined;
100
+ if (Array.isArray(members) && members.length > 0 && !meta.version_group) {
101
+ const cluster = [entry];
102
+ for (const memberId of members) {
103
+ const member = await store.getById(memberId);
104
+ if (member) cluster.push(member);
105
+ }
106
+ if (cluster.length >= 2) {
107
+ clusterMap.set(entry.id, cluster);
108
+ }
109
+ }
110
+ } catch { /* skip */ }
111
+ }
112
+
113
+ for (const [, cluster] of clusterMap) {
114
+ const decision = await evaluateCluster(llm, cluster);
115
+ const merges = await executeMergeDecisions(store, cluster, decision, scope);
116
+ llmMerges += merges;
117
+ }
118
+
119
+ if (llmMerges > 0) {
120
+ result.mergedCount += llmMerges;
121
+ }
122
+ }
123
+
124
+ return { triggered: true, consolidation: result };
125
+ }
126
+
127
+ /**
128
+ * Reset state (for testing).
129
+ */
130
+ export function resetConsolidationState(): void {
131
+ lastRunTimestamp = 0;
132
+ lastRunMemoryCount = 0;
133
+ }
package/src/auto-gc.ts ADDED
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Auto Garbage Collection — Brain-inspired active forgetting.
3
+ *
4
+ * Condition-driven trigger (not cron): runs when memory count exceeds threshold
5
+ * AND enough time has passed since last GC. Inspired by NREM triple-coupling:
6
+ * multiple conditions must be satisfied before consolidation fires.
7
+ *
8
+ * Wraps existing cleanup scripts into a programmatic API.
9
+ */
10
+
11
+ import type { MemoryStore } from "./store.js";
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Config
15
+ // ---------------------------------------------------------------------------
16
+
17
+ export interface AutoGcConfig {
18
+ /** Minimum memories before GC can trigger (default: 1000) */
19
+ minMemoryCount: number;
20
+ /** Minimum hours since last GC (default: 24) */
21
+ minHoursSinceLastGc: number;
22
+ /** Score below which memories are candidates for archival (default: 0.2) */
23
+ staleScoreThreshold: number;
24
+ /** Max entries to archive per run (default: 100) */
25
+ maxArchivePerRun: number;
26
+ }
27
+
28
+ export const DEFAULT_AUTO_GC_CONFIG: AutoGcConfig = {
29
+ minMemoryCount: 1000,
30
+ minHoursSinceLastGc: 24,
31
+ staleScoreThreshold: 0.2,
32
+ maxArchivePerRun: 100,
33
+ };
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // State
37
+ // ---------------------------------------------------------------------------
38
+
39
+ let lastGcTimestamp = 0;
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Core
43
+ // ---------------------------------------------------------------------------
44
+
45
+ export interface GcResult {
46
+ triggered: boolean;
47
+ reason?: string;
48
+ archivedCount: number;
49
+ totalChecked: number;
50
+ }
51
+
52
+ /**
53
+ * Check conditions and run GC if all thresholds are met.
54
+ * Returns immediately if conditions not met (no-op).
55
+ */
56
+ export async function maybeRunGc(
57
+ store: MemoryStore,
58
+ config: AutoGcConfig = DEFAULT_AUTO_GC_CONFIG,
59
+ ): Promise<GcResult> {
60
+ const stats = await store.stats();
61
+ const totalMemories = stats.total ?? 0;
62
+
63
+ // Condition 1: enough memories to warrant GC
64
+ if (totalMemories < config.minMemoryCount) {
65
+ return { triggered: false, reason: "below_memory_threshold", archivedCount: 0, totalChecked: 0 };
66
+ }
67
+
68
+ // Condition 2: enough time since last GC
69
+ const hoursSinceLastGc = (Date.now() - lastGcTimestamp) / 3_600_000;
70
+ if (hoursSinceLastGc < config.minHoursSinceLastGc) {
71
+ return { triggered: false, reason: "too_soon", archivedCount: 0, totalChecked: 0 };
72
+ }
73
+
74
+ // All conditions met — run GC
75
+ lastGcTimestamp = Date.now();
76
+
77
+ // Find stale peripheral memories with low importance and no recent access
78
+ const cutoffMs = Date.now() - 60 * 86_400_000; // 60 days ago
79
+ let archivedCount = 0;
80
+ let totalChecked = 0;
81
+
82
+ // Scan memories by listing them
83
+ const entries = await store.list({ limit: 5000 });
84
+ totalChecked = entries.length;
85
+
86
+ for (const entry of entries) {
87
+ if (archivedCount >= config.maxArchivePerRun) break;
88
+
89
+ let meta: Record<string, any> = {};
90
+ try { meta = JSON.parse(entry.metadata || "{}"); } catch { /* skip */ }
91
+
92
+ const isArchived = meta.archived === true;
93
+ if (isArchived) continue;
94
+
95
+ const tier = meta.tier || "peripheral";
96
+ const accessCount = meta.accessCount || 0;
97
+ const importance = entry.importance ?? 0;
98
+ const age = Date.now() - entry.timestamp;
99
+ const ageDays = age / 86_400_000;
100
+
101
+ // Archive criteria: peripheral + old + low importance + rarely accessed
102
+ if (
103
+ tier === "peripheral" &&
104
+ ageDays > 60 &&
105
+ importance < config.staleScoreThreshold &&
106
+ accessCount < 2
107
+ ) {
108
+ meta.archived = true;
109
+ meta.archivedAt = new Date().toISOString();
110
+ meta.archivedReason = "auto-gc:stale-peripheral";
111
+ await store.update(entry.id, { metadata: JSON.stringify(meta) });
112
+ archivedCount++;
113
+ }
114
+ }
115
+
116
+ return { triggered: true, archivedCount, totalChecked };
117
+ }
118
+
119
+ /**
120
+ * Reset last GC timestamp (for testing).
121
+ */
122
+ export function resetGcTimestamp(): void {
123
+ lastGcTimestamp = 0;
124
+ }
@@ -0,0 +1,95 @@
1
+ import { composeResumeContext, type ResumeContextDeps } from "./context-composer.js";
2
+ import type { DurableMemoryCategory } from "./memory-schema.js";
3
+ import type { RetrievalResult } from "./retriever.js";
4
+ import { buildRetrievalContext, resolveScopeSelection } from "./scope-policy.js";
5
+ import type { RetrievalProfileName, ResumeContextResponse } from "./session-schema.js";
6
+
7
+ export interface AutoRecallRequest {
8
+ message: string;
9
+ task?: string;
10
+ scope?: string;
11
+ sessionId?: string;
12
+ allScopes?: boolean;
13
+ limit?: number;
14
+ limitPerSection?: number;
15
+ includeLatestCheckpoint?: boolean;
16
+ category?: DurableMemoryCategory;
17
+ profile?: RetrievalProfileName;
18
+ env?: NodeJS.ProcessEnv;
19
+ operation?: string;
20
+ }
21
+
22
+ export interface AutoRecallResponse {
23
+ mode: "resume-only" | "resume+search";
24
+ resolvedScope?: string;
25
+ resume: ResumeContextResponse;
26
+ results: RetrievalResult[];
27
+ searchSkippedReason?: string;
28
+ }
29
+
30
+ export async function runAutoRecall(
31
+ deps: ResumeContextDeps,
32
+ request: AutoRecallRequest,
33
+ ): Promise<AutoRecallResponse> {
34
+ const message = request.message.replace(/\s+/g, " ").trim();
35
+ const task = (request.task || message).replace(/\s+/g, " ").trim();
36
+
37
+ if (!message) {
38
+ throw new Error("message is required");
39
+ }
40
+
41
+ const scopeSelection = resolveScopeSelection({
42
+ scope: request.scope,
43
+ sessionId: request.sessionId,
44
+ allScopes: request.allScopes,
45
+ operation: request.operation || "auto_recall",
46
+ env: request.env,
47
+ allowUnscoped: true,
48
+ });
49
+ const resumeScope = scopeSelection.inferredFrom === "sessionId"
50
+ ? undefined
51
+ : scopeSelection.resolvedScope;
52
+
53
+ const resume = await composeResumeContext(deps, {
54
+ task,
55
+ scope: resumeScope,
56
+ sessionId: request.sessionId,
57
+ limitPerSection: request.limitPerSection,
58
+ includeLatestCheckpoint: request.includeLatestCheckpoint,
59
+ profile: request.profile,
60
+ });
61
+
62
+ const resolvedScope = request.allScopes
63
+ ? undefined
64
+ : (resume.resolvedScope || resume.latestCheckpoint?.resolvedScope || scopeSelection.resolvedScope);
65
+ if (!request.allScopes && !resolvedScope) {
66
+ return {
67
+ mode: "resume-only",
68
+ resolvedScope,
69
+ resume,
70
+ results: [],
71
+ searchSkippedReason: "No explicit or inferred scope available for focused recall; returning resume context only.",
72
+ };
73
+ }
74
+
75
+ const results = await deps.retriever.retrieve(buildRetrievalContext({
76
+ query: message,
77
+ limit: request.limit || 5,
78
+ category: request.category,
79
+ scope: resolvedScope,
80
+ sessionId: request.sessionId,
81
+ allScopes: request.allScopes,
82
+ source: "auto-recall",
83
+ }, {
84
+ operation: request.operation || "auto_recall",
85
+ env: request.env,
86
+ allowUnscoped: request.allScopes === true,
87
+ }));
88
+
89
+ return {
90
+ mode: "resume+search",
91
+ resolvedScope,
92
+ resume,
93
+ results,
94
+ };
95
+ }