@smyslenny/agent-memory 5.0.0 → 5.0.2

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.
@@ -10,7 +10,7 @@ jobs:
10
10
  strategy:
11
11
  fail-fast: false
12
12
  matrix:
13
- node: [18, 20, 22]
13
+ node: [18, 20, 22, 24]
14
14
  steps:
15
15
  - uses: actions/checkout@v4
16
16
  - uses: actions/setup-node@v4
@@ -0,0 +1 @@
1
+ {"better-sqlite3@11.10.0": true, "esbuild@0.27.4": true}
package/CHANGELOG.md CHANGED
@@ -1,5 +1,120 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.0.1 (2026-03-20)
4
+
5
+ ### 🐛 Fixes
6
+
7
+ - **auto-ingest**: Daily log files (`YYYY-MM-DD.md`) are now skipped by default.
8
+ Only `MEMORY.md` (curated memory) is watched and ingested. Daily logs are raw
9
+ journals that often contain noise — they should be processed through the
10
+ memory-sync cron pipeline instead.
11
+ - New environment variable `AGENT_MEMORY_AUTO_INGEST_DAILY=1` restores the
12
+ previous behavior of ingesting all `.md` files in the `memory/` directory.
13
+
14
+ ## 5.0.0 (2026-03-20)
15
+
16
+ ### 🧠 Memory Intelligence
17
+
18
+ v5 is a major feature release that adds six intelligence capabilities to the
19
+ memory layer. All features are backward-compatible with v4 workflows.
20
+
21
+ Design document: `docs/design/0018-v5-memory-intelligence.md`
22
+
23
+ #### F1: Memory Links (记忆关联)
24
+
25
+ - Automatic link creation during `syncOne()`: after a successful `add` or
26
+ `merge`, candidates with `dedup_score ∈ [0.45, 0.82)` are saved as `related`
27
+ links (up to 5 per memory)
28
+ - `recall` and `surface` accept a new `related: boolean` parameter. When true,
29
+ top-K results are expanded with linked memories from the `links` table
30
+ (capped at `limit * 1.5`, with score scaled by `original_score * link_weight * 0.6`)
31
+ - Related memories are tagged with `match_type: 'related'` and
32
+ `related_source_id` in results so the agent knows why they appeared
33
+ - New MCP tool **`link`**: manually create or remove associations
34
+ (`relation`: `related` | `supersedes` | `contradicts`, with optional `weight`)
35
+
36
+ #### F2: Conflict Detection (冲突检测)
37
+
38
+ - Write Guard (`guard.ts`) now iterates over multiple candidates instead of
39
+ only the top-1 match
40
+ - Three conflict signal types detected between incoming content and existing
41
+ candidates:
42
+ - **Negation**: one side contains negation words the other does not
43
+ - **Value**: same entity with different numeric values (IPs, ports, versions)
44
+ - **Status**: one side marked done/cancelled while the other is in-progress
45
+ - Conflict score (0–1) is computed from weighted signals. Conflicts above 0.5
46
+ are reported in `GuardResult.conflicts` and propagated to `SyncResult`
47
+ - **Conflict Override rule**: when `dedup_score ≥ 0.93` and a `status` or
48
+ `value` conflict is detected, the guard action is forced from `skip` to
49
+ `update` — preventing legitimate state changes (e.g. TODO → DONE) from being
50
+ silently deduplicated. `negation` conflicts do not trigger override (higher
51
+ false-positive rate)
52
+ - Writes are never blocked by conflict detection — the agent decides what to do
53
+
54
+ #### F3: Temporal Recall (时间维度召回)
55
+
56
+ - `recall` and `surface` accept new optional parameters:
57
+ - `after` / `before` (ISO 8601) — time-range filter at the SQL layer for
58
+ both BM25 and vector search paths
59
+ - `recency_boost` (0–1) — blends a recency decay signal into the fusion
60
+ score: `final = (1 - boost) * base + boost * e^(-days/30)`
61
+ - BM25 and vector search functions (`searchBM25`, `searchByVector`) extended
62
+ with `after` / `before` filter support
63
+
64
+ #### F4: Passive Feedback (被动反馈)
65
+
66
+ - `FeedbackSource` type extended to `"recall" | "surface" | "passive"`
67
+ - When `recall` records access, the top-3 results automatically receive a
68
+ positive passive feedback event (value 0.7, vs 1.0 for explicit feedback)
69
+ - Rate-limited: max 3 passive feedback events per memory per 24-hour window
70
+ - Anti-N+1: deduplication check uses a single batch `WHERE memory_id IN (...)`
71
+ query instead of per-memory `SELECT COUNT(*)`
72
+
73
+ #### F5: Semantic Decay (语义衰减)
74
+
75
+ - New `isStaleContent(content, type)` function in `tidy.ts` detects
76
+ temporally-stale content via keyword pattern matching
77
+ - Pattern sets are scoped by memory type:
78
+ - `event`: broad matching (e.g. `正在`, `in progress`, `TODO`, `just now`)
79
+ - `knowledge`: anchored-start-only patterns (e.g. `^TODO:`, `^WIP:`) to
80
+ avoid false positives on knowledge descriptions containing those words
81
+ - `identity` and `emotion`: exempt from semantic decay
82
+ - Age thresholds: `in_progress` > 7d, `pending` > 14d, `ephemeral` > 3d
83
+ - Matched memories have their `vitality` multiplied by the pattern's
84
+ `decay_factor`
85
+ - `TidyResult` now includes `staleDecayed` count
86
+
87
+ #### F6: Memory Provenance (记忆溯源)
88
+
89
+ - Schema migration v6 → v7: three new nullable columns on `memories`:
90
+ - `source_session` — originating session ID
91
+ - `source_context` — trigger context (≤200 chars)
92
+ - `observed_at` — when the event actually happened (distinct from write time)
93
+ - `Memory` interface and `CreateMemoryInput` updated with provenance fields
94
+ - MCP `remember` tool accepts `session_id`, `context`, `observed_at`
95
+ - `recall` / `surface` results include provenance fields when present
96
+ - `guard.ts` `timeProximity()` now prefers `observed_at` over regex-guessed
97
+ timestamps from content/URI/source
98
+
99
+ ### 🧰 Tooling
100
+
101
+ - MCP toolset expanded from **10 → 11 tools** (added `link`)
102
+ - MCP server version string updated to `5.0.0`
103
+
104
+ ### ✅ Tests
105
+
106
+ - Added `tests/v5/intelligence.test.ts` with **25 new test cases** covering
107
+ all six v5 features
108
+ - Total test count: **96** (up from 69 in v4.2)
109
+
110
+ ### 📦 Schema
111
+
112
+ - Database schema version: **7** (from 6)
113
+ - Migration is additive (nullable columns only) — safe to upgrade in place
114
+ - Rollback: ignore new columns, delete new link/feedback rows by type
115
+
116
+ ---
117
+
3
118
  ## 4.2.0 (2026-03-19)
4
119
 
5
120
  ### 🛡️ Anti-Noise Hardening
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
  <a href="https://www.npmjs.com/package/@smyslenny/agent-memory"><img src="https://img.shields.io/npm/v/@smyslenny/agent-memory" alt="npm" /></a>
11
11
  <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT" /></a>
12
12
  <a href="https://nodejs.org/"><img src="https://img.shields.io/badge/Node.js-%E2%89%A518-green.svg" alt="Node.js" /></a>
13
- <a href="https://modelcontextprotocol.io/"><img src="https://img.shields.io/badge/MCP-10_tools-orange.svg" alt="MCP" /></a>
13
+ <a href="https://modelcontextprotocol.io/"><img src="https://img.shields.io/badge/MCP-11_tools-orange.svg" alt="MCP" /></a>
14
14
  </p>
15
15
 
16
16
  **English** | [简体中文说明](docs/README-zh.md)
@@ -22,7 +22,7 @@ AgentMemory is a SQLite-first memory layer for AI agents. It lets an agent:
22
22
  - **maintain** them over time with `reflect`, `reindex`, and feedback signals
23
23
  - **integrate** through **CLI**, **MCP stdio**, or **HTTP/SSE**
24
24
 
25
- Current release: **`4.3.0`**.
25
+ Current release: **`5.0.1`**.
26
26
 
27
27
  Without an embedding provider, AgentMemory still works in **BM25-only mode**.
28
28
  With one configured, it adds **hybrid recall** and **semantic dedup**.
@@ -40,16 +40,36 @@ That means it is designed around the things agent runtimes actually need:
40
40
  - a lifecycle path for decay, governance, reindexing, and recovery-friendly jobs
41
41
  - a local-first deployment model that stays useful even without extra infra
42
42
 
43
- Core building blocks in v4:
43
+ Core building blocks:
44
44
 
45
45
  - **Typed memories**: `identity`, `emotion`, `knowledge`, `event`
46
46
  - **URI paths** for stable addressing
47
- - **Write Guard** with semantic dedup + typed merge policy
47
+ - **Write Guard** with semantic dedup + typed merge policy + conflict detection
48
48
  - **Hybrid retrieval**: BM25 first, optional vector search
49
+ - **Memory links** with automatic association and related-memory expansion
50
+ - **Temporal recall** with time filtering and recency boost
49
51
  - **Context-aware surfacing** for task/recent-turn driven context injection
52
+ - **Passive feedback** that records usage signals automatically
53
+ - **Semantic decay** that detects stale content beyond pure time-based Ebbinghaus
54
+ - **Memory provenance** for tracking where and when each memory originated
50
55
  - **Lifecycle jobs**: `reflect`, `reindex`, job checkpoints, feedback signals
51
56
  - **Three transport modes**: CLI, MCP stdio, HTTP/SSE
52
57
 
58
+ ### New in v5: Memory Intelligence
59
+
60
+ v5 adds six features that turn agent-memory from a durable store into an
61
+ intelligent memory layer. All features are backward-compatible — existing
62
+ v4 workflows continue to work unchanged.
63
+
64
+ | Feature | What it does |
65
+ | --- | --- |
66
+ | **F1 Memory Links** | Automatically detects semantically related memories during write and builds lightweight associations. `recall` and `surface` support `related` expansion to pull in linked memories. A new `link` tool allows manual link management. |
67
+ | **F2 Conflict Detection** | Write Guard now scans candidates for contradictions (negation, value changes, status changes). Conflicts are reported in the sync result without blocking writes. A **Conflict Override** rule ensures status updates (e.g. TODO → DONE) are not incorrectly deduplicated. |
68
+ | **F3 Temporal Recall** | `recall` and `surface` accept `after`, `before`, and `recency_boost` parameters. Time filtering happens at the SQL layer for both BM25 and vector paths. Recency boost blends a time-decay signal into the fusion score. |
69
+ | **F4 Passive Feedback** | When `recall` returns results and records access, positive feedback is automatically logged for the top-3 hits. Rate-limited to 3 passive events per memory per 24 hours. |
70
+ | **F5 Semantic Decay** | The `tidy` phase now detects stale content through keyword pattern matching (e.g. "in progress", "TODO:", "just now"). Patterns are scoped by memory type — `event` uses broad matching, `knowledge` uses anchored-start-only patterns. `identity` and `emotion` are exempt. |
71
+ | **F6 Memory Provenance** | Memories can carry `source_session`, `source_context`, and `observed_at` metadata. This tracks where and when a memory originated, separate from its write timestamp. Schema migrated from v6 → v7. |
72
+
53
73
  ## 2) How is it different from a vector DB, a RAG pipeline, or memory summaries?
54
74
 
55
75
  | Thing | Good at | What AgentMemory adds |
@@ -177,18 +197,19 @@ npx agent-memory reflect all
177
197
  }
178
198
  ```
179
199
 
180
- Available MCP tools in v4:
200
+ Available MCP tools:
181
201
 
182
- - `remember`
183
- - `recall`
184
- - `recall_path`
185
- - `boot`
186
- - `forget`
187
- - `reflect`
188
- - `status`
189
- - `ingest`
190
- - `reindex`
191
- - `surface`
202
+ - `remember` — store a memory (supports provenance: `session_id`, `context`, `observed_at`)
203
+ - `recall` — hybrid search (supports `related`, `after`, `before`, `recency_boost`)
204
+ - `recall_path` — read or list memories by URI
205
+ - `boot` — load startup memories (narrative or JSON)
206
+ - `forget` — soft-decay or hard-delete a memory
207
+ - `reflect` — run sleep cycle phases (decay, tidy, govern)
208
+ - `status` — memory system statistics
209
+ - `ingest` — extract structured memories from markdown
210
+ - `reindex` — rebuild BM25 index and optional embeddings
211
+ - `surface` — context-aware readonly surfacing (supports `related`, `after`, `before`, `recency_boost`)
212
+ - `link` — manually create or remove associations between memories
192
213
 
193
214
  ### C. HTTP API
194
215
 
@@ -274,6 +295,22 @@ export AGENT_MEMORY_EMBEDDING_API_KEY=your-api-key
274
295
  Or use `AGENT_MEMORY_EMBEDDING_PROVIDER=local-http` for a local HTTP embedding
275
296
  service. If no provider is configured, AgentMemory falls back to BM25-only.
276
297
 
298
+ ## Environment variables
299
+
300
+ | Variable | Default | Description |
301
+ | --- | --- | --- |
302
+ | `AGENT_MEMORY_DB` | `./agent-memory.db` | SQLite database path |
303
+ | `AGENT_MEMORY_AGENT_ID` | `default` | Agent scope for multi-agent setups |
304
+ | `AGENT_MEMORY_MAX_MEMORIES` | `200` | Maximum memories retained during `reflect govern` |
305
+ | `AGENT_MEMORY_AUTO_INGEST` | `1` | Set to `0` to disable the auto-ingest file watcher |
306
+ | `AGENT_MEMORY_AUTO_INGEST_DAILY` | _(unset)_ | Set to `1` to include daily log files (`YYYY-MM-DD.md`) in auto-ingest. By default, only `MEMORY.md` is watched. |
307
+ | `AGENT_MEMORY_WORKSPACE` | `~/.openclaw/workspace` | Workspace directory for the auto-ingest watcher |
308
+ | `AGENT_MEMORY_EMBEDDING_PROVIDER` | _(unset)_ | `openai-compatible` or `local-http` |
309
+ | `AGENT_MEMORY_EMBEDDING_BASE_URL` | _(unset)_ | Base URL for the embedding endpoint |
310
+ | `AGENT_MEMORY_EMBEDDING_MODEL` | _(unset)_ | Embedding model name |
311
+ | `AGENT_MEMORY_EMBEDDING_DIMENSION` | _(unset)_ | Embedding vector dimension |
312
+ | `AGENT_MEMORY_EMBEDDING_API_KEY` | _(unset)_ | API key for the embedding provider |
313
+
277
314
  ## Documentation map
278
315
 
279
316
  - [Architecture](docs/architecture.md)
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // AgentMemory v2 — Sleep-cycle memory for AI agents
2
+ // AgentMemory — Sleep-cycle memory for AI agents
3
3
  var __defProp = Object.defineProperty;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __esm = (fn, res) => function __init() {
@@ -1649,21 +1649,20 @@ async function searchVectorBranch(db, query, opts) {
1649
1649
  before: opts.before
1650
1650
  });
1651
1651
  }
1652
- function expandRelated(db, results, agentId, maxTotal) {
1653
- const existingIds = new Set(results.map((r) => r.memory.id));
1652
+ function fetchRelatedLinks(db, sourceIds, agentId, excludeIds, maxPerSource = 5) {
1654
1653
  const related = [];
1655
- for (const result of results) {
1654
+ for (const sourceId of sourceIds) {
1656
1655
  const links = db.prepare(
1657
1656
  `SELECT l.target_id, l.weight, m.*
1658
1657
  FROM links l
1659
1658
  JOIN memories m ON m.id = l.target_id
1660
1659
  WHERE l.agent_id = ? AND l.source_id = ?
1661
1660
  ORDER BY l.weight DESC
1662
- LIMIT 5`
1663
- ).all(agentId, result.memory.id);
1661
+ LIMIT ?`
1662
+ ).all(agentId, sourceId, maxPerSource);
1664
1663
  for (const link of links) {
1665
- if (existingIds.has(link.target_id)) continue;
1666
- existingIds.add(link.target_id);
1664
+ if (excludeIds.has(link.target_id)) continue;
1665
+ excludeIds.add(link.target_id);
1667
1666
  const relatedMemory = {
1668
1667
  id: link.id,
1669
1668
  content: link.content,
@@ -1686,12 +1685,30 @@ function expandRelated(db, results, agentId, maxTotal) {
1686
1685
  };
1687
1686
  related.push({
1688
1687
  memory: relatedMemory,
1689
- score: result.score * link.weight * 0.6,
1690
- related_source_id: result.memory.id,
1691
- match_type: "related"
1688
+ sourceId,
1689
+ weight: link.weight
1692
1690
  });
1693
1691
  }
1694
1692
  }
1693
+ return related;
1694
+ }
1695
+ function expandRelated(db, results, agentId, maxTotal) {
1696
+ const existingIds = new Set(results.map((r) => r.memory.id));
1697
+ const links = fetchRelatedLinks(
1698
+ db,
1699
+ results.map((r) => r.memory.id),
1700
+ agentId,
1701
+ existingIds
1702
+ );
1703
+ const related = links.map((link) => {
1704
+ const sourceResult = results.find((r) => r.memory.id === link.sourceId);
1705
+ return {
1706
+ memory: link.memory,
1707
+ score: (sourceResult?.score ?? 0) * link.weight * 0.6,
1708
+ related_source_id: link.sourceId,
1709
+ match_type: "related"
1710
+ };
1711
+ });
1695
1712
  const directResults = results.map((r) => ({
1696
1713
  ...r,
1697
1714
  match_type: "direct"
@@ -2192,7 +2209,8 @@ async function surfaceMemories(db, input) {
2192
2209
  }),
2193
2210
  lexical_rank: signal.queryRank ?? signal.recentRank ?? signal.taskRank,
2194
2211
  semantic_rank: signal.semanticRank,
2195
- semantic_similarity: signal.semanticSimilarity
2212
+ semantic_similarity: signal.semanticSimilarity,
2213
+ match_type: "direct"
2196
2214
  };
2197
2215
  }).sort((left, right) => {
2198
2216
  if (right.score !== left.score) return right.score - left.score;
@@ -2201,6 +2219,42 @@ async function surfaceMemories(db, input) {
2201
2219
  if (left.memory.priority !== right.memory.priority) return left.memory.priority - right.memory.priority;
2202
2220
  return right.memory.updated_at.localeCompare(left.memory.updated_at);
2203
2221
  }).slice(0, limit);
2222
+ if (input.related) {
2223
+ const existingIds = new Set(results.map((r) => r.memory.id));
2224
+ const links = fetchRelatedLinks(
2225
+ db,
2226
+ results.map((r) => r.memory.id),
2227
+ agentId,
2228
+ existingIds
2229
+ );
2230
+ const relatedResults = links.map((link) => {
2231
+ const sourceResult = results.find((r) => r.memory.id === link.sourceId);
2232
+ const feedbackSummary = getFeedbackSummary(db, link.memory.id, agentId);
2233
+ return {
2234
+ memory: link.memory,
2235
+ score: (sourceResult?.score ?? 0) * link.weight * 0.6,
2236
+ semantic_score: 0,
2237
+ lexical_score: 0,
2238
+ task_match: 0,
2239
+ vitality: link.memory.vitality,
2240
+ priority_prior: priorityPrior(link.memory.priority),
2241
+ feedback_score: feedbackSummary.score,
2242
+ feedback_summary: feedbackSummary,
2243
+ reason_codes: [`type:${link.memory.type}`, "related"],
2244
+ related_source_id: link.sourceId,
2245
+ match_type: "related"
2246
+ };
2247
+ });
2248
+ const maxTotal = Math.floor(limit * 1.5);
2249
+ const combined = [...results, ...relatedResults].sort((a, b) => b.score - a.score).slice(0, maxTotal);
2250
+ return {
2251
+ count: combined.length,
2252
+ query: trimmedQuery,
2253
+ task: trimmedTask,
2254
+ intent: input.intent,
2255
+ results: combined
2256
+ };
2257
+ }
2204
2258
  return {
2205
2259
  count: results.length,
2206
2260
  query: trimmedQuery,
@@ -3357,6 +3411,8 @@ function formatRecallResponse(result) {
3357
3411
  vector_rank: row.vector_rank,
3358
3412
  bm25_score: row.bm25_score,
3359
3413
  vector_score: row.vector_score,
3414
+ related_source_id: row.related_source_id,
3415
+ match_type: row.match_type,
3360
3416
  updated_at: row.memory.updated_at
3361
3417
  }))
3362
3418
  };
@@ -3381,6 +3437,8 @@ function formatSurfaceResponse(result) {
3381
3437
  feedback_score: row.feedback_score,
3382
3438
  feedback_summary: row.feedback_summary,
3383
3439
  reason_codes: row.reason_codes,
3440
+ related_source_id: row.related_source_id,
3441
+ match_type: row.match_type,
3384
3442
  updated_at: row.memory.updated_at
3385
3443
  }))
3386
3444
  };
@@ -3550,7 +3608,11 @@ function createHttpServer(options) {
3550
3608
  emotion_val: asNumber(body.emotion_val),
3551
3609
  agent_id: asString(body.agent_id) ?? defaultAgentId,
3552
3610
  conservative: asBoolean(body.conservative),
3553
- provider: options?.provider
3611
+ provider: options?.provider,
3612
+ emotion_tag: asString(body.emotion_tag),
3613
+ source_session: asString(body.source_session),
3614
+ source_context: asString(body.source_context),
3615
+ observed_at: asString(body.observed_at)
3554
3616
  });
3555
3617
  sendJson(res, 200, result);
3556
3618
  return;
@@ -3565,7 +3627,12 @@ function createHttpServer(options) {
3565
3627
  query,
3566
3628
  limit: asNumber(body.limit),
3567
3629
  agent_id: asString(body.agent_id) ?? defaultAgentId,
3568
- provider: options?.provider
3630
+ provider: options?.provider,
3631
+ related: asBoolean(body.related),
3632
+ after: asString(body.after),
3633
+ before: asString(body.before),
3634
+ recency_boost: asNumber(body.recency_boost),
3635
+ emotion_tag: asString(body.emotion_tag)
3569
3636
  });
3570
3637
  sendJson(res, 200, formatRecallResponse(result));
3571
3638
  return;
@@ -3585,7 +3652,12 @@ function createHttpServer(options) {
3585
3652
  types,
3586
3653
  limit: asNumber(body.limit),
3587
3654
  agent_id: asString(body.agent_id) ?? defaultAgentId,
3588
- provider: options?.provider
3655
+ provider: options?.provider,
3656
+ related: asBoolean(body.related),
3657
+ after: asString(body.after),
3658
+ before: asString(body.before),
3659
+ recency_boost: asNumber(body.recency_boost),
3660
+ emotion_tag: asString(body.emotion_tag)
3589
3661
  });
3590
3662
  sendJson(res, 200, formatSurfaceResponse(result));
3591
3663
  return;
@@ -3719,7 +3791,7 @@ function getAgentId() {
3719
3791
  }
3720
3792
  function printHelp() {
3721
3793
  console.log(`
3722
- \u{1F9E0} AgentMemory v4 \u2014 Sleep-cycle memory for AI agents
3794
+ \u{1F9E0} AgentMemory \u2014 Sleep-cycle memory for AI agents
3723
3795
 
3724
3796
  Usage: agent-memory <command> [options]
3725
3797