@smyslenny/agent-memory 4.1.0-alpha.1 → 4.3.0
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.
- package/CHANGELOG.md +38 -0
- package/README.md +1 -1
- package/dist/bin/agent-memory.js +70 -15
- package/dist/bin/agent-memory.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +96 -15
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +96 -15
- package/dist/mcp/server.js.map +1 -1
- package/package.json +1 -1
- package/docs/design/.next-id +0 -1
- package/docs/design/0004-agent-memory-integration.md +0 -316
- package/docs/design/0005-reranker-api-integration.md +0 -276
- package/docs/design/0006-multi-provider-embedding.md +0 -196
- package/docs/design/0014-memory-core-dedup.md +0 -722
- package/docs/design/0015-v4-overhaul.md +0 -631
- package/docs/design/0016-v41-warm-boot-surface-emotion.md +0 -228
- package/docs/design/TEMPLATE.md +0 -67
- package/docs/roadmap/integration-plan-v1.md +0 -139
- package/docs/roadmap/memory-architecture.md +0 -168
- package/docs/roadmap/warm-boot.md +0 -135
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.2.0 (2026-03-19)
|
|
4
|
+
|
|
5
|
+
### 🛡️ Anti-Noise Hardening
|
|
6
|
+
|
|
7
|
+
This release addresses the "heartbeat flood" problem where memory-sync cron
|
|
8
|
+
ingested hundreds of low-value status observations (e.g. "HEARTBEAT_OK",
|
|
9
|
+
"安静模式", "PR 无变化") into the curated memory store.
|
|
10
|
+
|
|
11
|
+
#### Guard improvements
|
|
12
|
+
- **Raised specificity threshold** for P2/P3 memories from 8 to 20 effective
|
|
13
|
+
characters
|
|
14
|
+
- **CJK-aware length calculation**: CJK characters count as 3 effective chars
|
|
15
|
+
(reflecting their higher information density vs ASCII), preventing false
|
|
16
|
+
rejections of legitimate Chinese content
|
|
17
|
+
|
|
18
|
+
#### Ingest noise filter
|
|
19
|
+
- Added `isIngestNoise()` pre-filter in `extractIngestItems()` that skips lines
|
|
20
|
+
matching known noise patterns before they reach the Write Guard:
|
|
21
|
+
- Heartbeat status: `HEARTBEAT_OK`, `安静模式`, `不打扰`, `继续安静待命`
|
|
22
|
+
- Empty deltas: `无新 delta`, `无变化`, `无紧急`, `无新进展`
|
|
23
|
+
- System dumps: `openclaw status`, `openclaw gateway status`, `session_status`
|
|
24
|
+
- Stale PR observations: `PR #NNN 无变化`, `基线未变`, `轻量复查`
|
|
25
|
+
- Cron noise: `cron 会话`, `距上次心跳`, `危险区协议`
|
|
26
|
+
|
|
27
|
+
#### Tidy expansion
|
|
28
|
+
- `getDecayedMemories()` now includes **P2 (knowledge)** in cleanup candidates
|
|
29
|
+
(previously only P3 event). This means decayed low-vitality knowledge entries
|
|
30
|
+
will be cleaned up during sleep tidy, not just events.
|
|
31
|
+
|
|
32
|
+
#### Govern env config
|
|
33
|
+
- `maxMemories` can now be set via `AGENT_MEMORY_MAX_MEMORIES` environment
|
|
34
|
+
variable (default: 200)
|
|
35
|
+
|
|
36
|
+
### ✅ Tests
|
|
37
|
+
- 66 tests passing (19 files)
|
|
38
|
+
- Added `tests/ingest/noise-filter.test.ts` covering heartbeat noise rejection,
|
|
39
|
+
meaningful content preservation, and mixed signal/noise handling
|
|
40
|
+
|
|
3
41
|
## 4.0.0-alpha.1 (2026-03-09)
|
|
4
42
|
|
|
5
43
|
### 🚀 Repositioning
|
package/README.md
CHANGED
|
@@ -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.
|
|
25
|
+
Current release: **`4.3.0`**.
|
|
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**.
|
package/dist/bin/agent-memory.js
CHANGED
|
@@ -691,6 +691,44 @@ function createLocalHttpEmbeddingProvider(opts) {
|
|
|
691
691
|
}
|
|
692
692
|
};
|
|
693
693
|
}
|
|
694
|
+
function createGeminiEmbeddingProvider(opts) {
|
|
695
|
+
const id = stableProviderId(`gemini:${opts.model}`, `${opts.model}|${opts.dimension}`);
|
|
696
|
+
return {
|
|
697
|
+
id,
|
|
698
|
+
model: opts.model,
|
|
699
|
+
dimension: opts.dimension,
|
|
700
|
+
async embed(texts) {
|
|
701
|
+
if (texts.length === 0) return [];
|
|
702
|
+
const fetchFn = getFetch(opts.fetchImpl);
|
|
703
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/models/${opts.model}:batchEmbedContents?key=${opts.apiKey}`;
|
|
704
|
+
const requests = texts.map((text) => ({
|
|
705
|
+
model: `models/${opts.model}`,
|
|
706
|
+
content: { parts: [{ text }] },
|
|
707
|
+
outputDimensionality: opts.dimension
|
|
708
|
+
}));
|
|
709
|
+
const response = await fetchFn(url, {
|
|
710
|
+
method: "POST",
|
|
711
|
+
headers: { "content-type": "application/json" },
|
|
712
|
+
body: JSON.stringify({ requests })
|
|
713
|
+
});
|
|
714
|
+
if (!response.ok) {
|
|
715
|
+
const detail = await response.text().catch(() => "");
|
|
716
|
+
throw new Error(`Gemini embedding request failed: ${response.status} ${response.statusText}${detail ? ` \u2014 ${detail}` : ""}`);
|
|
717
|
+
}
|
|
718
|
+
const json2 = await response.json();
|
|
719
|
+
const embeddings = json2?.embeddings;
|
|
720
|
+
if (!Array.isArray(embeddings)) {
|
|
721
|
+
throw new Error("Gemini embedding response missing embeddings array");
|
|
722
|
+
}
|
|
723
|
+
return embeddings.map(
|
|
724
|
+
(entry, index) => assertEmbeddingVector(entry?.values, opts.dimension, `Gemini embedding item ${index}`)
|
|
725
|
+
);
|
|
726
|
+
},
|
|
727
|
+
async healthcheck() {
|
|
728
|
+
await this.embed(["healthcheck"]);
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
}
|
|
694
732
|
function normalizeEmbeddingBaseUrl(baseUrl) {
|
|
695
733
|
return trimTrailingSlashes(baseUrl);
|
|
696
734
|
}
|
|
@@ -708,7 +746,7 @@ function parseDimension(raw) {
|
|
|
708
746
|
}
|
|
709
747
|
function parseProvider(raw) {
|
|
710
748
|
if (!raw) return null;
|
|
711
|
-
if (raw === "openai-compatible" || raw === "local-http") {
|
|
749
|
+
if (raw === "openai-compatible" || raw === "local-http" || raw === "gemini") {
|
|
712
750
|
return raw;
|
|
713
751
|
}
|
|
714
752
|
throw new Error(`Unsupported embedding provider: ${raw}`);
|
|
@@ -719,8 +757,8 @@ function getEmbeddingProviderConfigFromEnv(env = process.env) {
|
|
|
719
757
|
const baseUrl = env.AGENT_MEMORY_EMBEDDING_BASE_URL;
|
|
720
758
|
const model = env.AGENT_MEMORY_EMBEDDING_MODEL;
|
|
721
759
|
const dimension = parseDimension(env.AGENT_MEMORY_EMBEDDING_DIMENSION);
|
|
722
|
-
if (!baseUrl) {
|
|
723
|
-
throw new Error("AGENT_MEMORY_EMBEDDING_BASE_URL is required when embeddings are enabled");
|
|
760
|
+
if (!baseUrl && provider !== "gemini") {
|
|
761
|
+
throw new Error("AGENT_MEMORY_EMBEDDING_BASE_URL is required when embeddings are enabled (not needed for gemini provider)");
|
|
724
762
|
}
|
|
725
763
|
if (!model) {
|
|
726
764
|
throw new Error("AGENT_MEMORY_EMBEDDING_MODEL is required when embeddings are enabled");
|
|
@@ -731,9 +769,12 @@ function getEmbeddingProviderConfigFromEnv(env = process.env) {
|
|
|
731
769
|
if (provider === "openai-compatible" && !env.AGENT_MEMORY_EMBEDDING_API_KEY) {
|
|
732
770
|
throw new Error("AGENT_MEMORY_EMBEDDING_API_KEY is required for openai-compatible providers");
|
|
733
771
|
}
|
|
772
|
+
if (provider === "gemini" && !env.AGENT_MEMORY_EMBEDDING_API_KEY) {
|
|
773
|
+
throw new Error("AGENT_MEMORY_EMBEDDING_API_KEY is required for gemini provider (Google AI API key)");
|
|
774
|
+
}
|
|
734
775
|
return {
|
|
735
776
|
provider,
|
|
736
|
-
baseUrl,
|
|
777
|
+
baseUrl: baseUrl ?? "",
|
|
737
778
|
model,
|
|
738
779
|
dimension,
|
|
739
780
|
apiKey: env.AGENT_MEMORY_EMBEDDING_API_KEY
|
|
@@ -742,8 +783,16 @@ function getEmbeddingProviderConfigFromEnv(env = process.env) {
|
|
|
742
783
|
function createEmbeddingProvider(input, opts) {
|
|
743
784
|
const normalized = {
|
|
744
785
|
...input,
|
|
745
|
-
baseUrl: normalizeEmbeddingBaseUrl(input.baseUrl)
|
|
786
|
+
baseUrl: input.baseUrl ? normalizeEmbeddingBaseUrl(input.baseUrl) : ""
|
|
746
787
|
};
|
|
788
|
+
if (normalized.provider === "gemini") {
|
|
789
|
+
return createGeminiEmbeddingProvider({
|
|
790
|
+
model: normalized.model,
|
|
791
|
+
dimension: normalized.dimension,
|
|
792
|
+
apiKey: normalized.apiKey,
|
|
793
|
+
fetchImpl: opts?.fetchImpl
|
|
794
|
+
});
|
|
795
|
+
}
|
|
747
796
|
if (normalized.provider === "openai-compatible") {
|
|
748
797
|
return createOpenAICompatibleEmbeddingProvider({
|
|
749
798
|
baseUrl: normalized.baseUrl,
|
|
@@ -770,13 +819,16 @@ function resolveEmbeddingProviderConfig(opts) {
|
|
|
770
819
|
const model = opts?.config?.model ?? envConfig?.model;
|
|
771
820
|
const dimension = opts?.config?.dimension ?? envConfig?.dimension;
|
|
772
821
|
const apiKey = opts?.config?.apiKey ?? envConfig?.apiKey;
|
|
773
|
-
if (!provider || !
|
|
822
|
+
if (!provider || !model || !dimension) {
|
|
774
823
|
throw new Error("Incomplete embedding provider configuration");
|
|
775
824
|
}
|
|
776
|
-
if (provider
|
|
777
|
-
throw new Error("
|
|
825
|
+
if (provider !== "gemini" && !baseUrl) {
|
|
826
|
+
throw new Error("baseUrl is required for non-gemini embedding providers");
|
|
827
|
+
}
|
|
828
|
+
if ((provider === "openai-compatible" || provider === "gemini") && !apiKey) {
|
|
829
|
+
throw new Error(`${provider} embedding provider requires an API key`);
|
|
778
830
|
}
|
|
779
|
-
return { provider, baseUrl, model, dimension, apiKey };
|
|
831
|
+
return { provider, baseUrl: baseUrl ?? "", model, dimension, apiKey };
|
|
780
832
|
}
|
|
781
833
|
function getEmbeddingProvider(opts) {
|
|
782
834
|
const config = resolveEmbeddingProviderConfig({ config: opts?.config, env: opts?.env });
|
|
@@ -2187,9 +2239,11 @@ function fourCriterionGate(input) {
|
|
|
2187
2239
|
const content = input.content.trim();
|
|
2188
2240
|
const failed = [];
|
|
2189
2241
|
const priority = input.priority ?? (input.type === "identity" ? 0 : input.type === "emotion" ? 1 : input.type === "knowledge" ? 2 : 3);
|
|
2190
|
-
const
|
|
2191
|
-
const
|
|
2192
|
-
|
|
2242
|
+
const cjkCount = (content.match(/[\u4e00-\u9fff\u3400-\u4dbf]/g) ?? []).length;
|
|
2243
|
+
const effectiveLength = content.length + cjkCount * 2;
|
|
2244
|
+
const minLength = priority <= 1 ? 4 : 20;
|
|
2245
|
+
const specificity = effectiveLength >= minLength ? Math.min(1, effectiveLength / 50) : 0;
|
|
2246
|
+
if (specificity === 0) failed.push(`specificity (too short: effective ${effectiveLength} < ${minLength} chars)`);
|
|
2193
2247
|
const tokens = tokenize(content);
|
|
2194
2248
|
const novelty = tokens.length >= 1 ? Math.min(1, tokens.length / 5) : 0;
|
|
2195
2249
|
if (novelty === 0) failed.push("novelty (no meaningful tokens after filtering)");
|
|
@@ -2420,9 +2474,9 @@ function getDecayedMemories(db, threshold = 0.05, opts) {
|
|
|
2420
2474
|
const agentId = opts?.agent_id;
|
|
2421
2475
|
return db.prepare(
|
|
2422
2476
|
agentId ? `SELECT id, content, vitality, priority FROM memories
|
|
2423
|
-
WHERE vitality < ? AND priority >=
|
|
2477
|
+
WHERE vitality < ? AND priority >= 2 AND agent_id = ?
|
|
2424
2478
|
ORDER BY vitality ASC` : `SELECT id, content, vitality, priority FROM memories
|
|
2425
|
-
WHERE vitality < ? AND priority >=
|
|
2479
|
+
WHERE vitality < ? AND priority >= 2
|
|
2426
2480
|
ORDER BY vitality ASC`
|
|
2427
2481
|
).all(...agentId ? [threshold, agentId] : [threshold]);
|
|
2428
2482
|
}
|
|
@@ -2520,7 +2574,8 @@ function rankEvictionCandidates(db, opts) {
|
|
|
2520
2574
|
}
|
|
2521
2575
|
function runGovern(db, opts) {
|
|
2522
2576
|
const agentId = opts?.agent_id;
|
|
2523
|
-
const
|
|
2577
|
+
const envMax = Number.parseInt(process.env.AGENT_MEMORY_MAX_MEMORIES ?? "", 10);
|
|
2578
|
+
const maxMemories = opts?.maxMemories ?? (Number.isFinite(envMax) && envMax > 0 ? envMax : 200);
|
|
2524
2579
|
let orphanPaths = 0;
|
|
2525
2580
|
let emptyMemories = 0;
|
|
2526
2581
|
let evicted = 0;
|