Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
31d9f95
Fix: 添加了未提取记忆补偿功能
TriDefender Aug 10, 2026
3df3614
Feat: 添加遗忘曲线门控
TriDefender Aug 11, 2026
d00440e
Merge branch 'adoresever:desktop-2.0' into desktop-2.0
TriDefender Aug 11, 2026
2f7fa58
Fix: Fixed tests
TriDefender Aug 11, 2026
dcfad1d
Fix: 确保跨插件重启可以继承状态
TriDefender Aug 12, 2026
8ccfb79
移植主干改动1fdec04,避免全量更新communities
TriDefender Aug 15, 2026
492923c
Update integration.graph.test.ts
TriDefender Aug 15, 2026
499cb76
Added finegrain memory control on cron
TriDefender Aug 15, 2026
02c4bb2
Fix: fixed incoherent tests
TriDefender Aug 15, 2026
d1a7604
Fix: 修了一些阻塞问题
TriDefender Aug 18, 2026
5101f7c
Fix: 进行代码审计后捉了几个bug
TriDefender Aug 18, 2026
bb124b7
Fix 修复代码审计发现的并发、数据损坏与性能缺陷
TriDefender Aug 23, 2026
6009c28
chore: updated docs
TriDefender Aug 31, 2026
c3f6748
feat: add LLM failure cooldown guard (persistent config errors only)
TriDefender Aug 31, 2026
0afefdc
fix: guard duplicate register() and accept lowercase llm.baseUrl
TriDefender Aug 31, 2026
bba42eb
feat: add opt-in bounded raw-message retention (messageRetention)
TriDefender Aug 31, 2026
d247624
Fix: 按照Openclaw 规范声明 transcript fencing 语义
TriDefender Sep 1, 2026
fcfec61
Fix: Test synced passwords
TriDefender Sep 1, 2026
6bd0d7e
Fix: Context engine "graph-memory-pro" factory returned null instead …
TriDefender Sep 1, 2026
42b18e0
chore: remove dead code — legacy store APIs bypassed by Neo4j version…
TriDefender Sep 2, 2026
b5a18b1
Removed plugin residuals
TriDefender Sep 2, 2026
ff3e787
Merge branch 'desktop-2.0' of https://github.com/TriDefender/graph-me…
TriDefender Sep 2, 2026
db20f0a
refactor: dedupe shared implementations (Fixes #2)
TriDefender Sep 2, 2026
0ca2a11
Merge pull request #3 from TriDefender/refactor/dedupe-issue-2
TriDefender Sep 2, 2026
0575239
feat: reembed pipeline — void stale vectors + batch rebuild (graph-me…
TriDefender Sep 2, 2026
580a4e6
Feat: 重做Deprecate+Delete业务逻辑
TriDefender Sep 3, 2026
be142d8
Update integration.neo4j.test.ts
TriDefender Sep 3, 2026
0c9b85a
fix: deprecate lifecycle — autoDeprecateNodes status guard + legacy d…
TriDefender Sep 3, 2026
c548f2b
feat: 提取/召回 LLM 与 embedding 成本优化套件
TriDefender Sep 3, 2026
5d2bc33
chore: Performed code quality review
TriDefender Sep 3, 2026
d38d972
chore: local code audit results
TriDefender Sep 3, 2026
82cf3c5
chore: 代码质量提高
TriDefender Sep 6, 2026
e3b87b0
适配webui使用:归档会话钩子进入使用(类似/new)
TriDefender Sep 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ jobs:
run: npm test
env:
NEO4J_INTEGRATION: "1"
# 集成测试(尤其删除类的 retention 测试)要求显式指定 URI;
# runner 上的 service container 是一次性实例,7687 安全
NEO4J_TEST_URI: bolt://localhost:7687
# 与 service 的 NEO4J_AUTH 保持同一来源,供可配置凭据的测试使用。
NEO4J_TEST_USER: neo4j
NEO4J_TEST_PASSWORD: graphmemory
CI: "true"

shellcheck:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ dist/
*.log
package-lock.json
.omo/
.zcode/
/migrate/staging
*.pyc
127 changes: 122 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ The installer configures Neo4j to start at boot with a 3-tier no-sudo fallback:

## Manual Configuration

Install the local plugin, then make it the OpenClaw context engine:
Install the local plugin, then make it the OpenClaw context engine (**restart the gateway after changing config**: the plugin guards against duplicate `register()` — a second registration without `dispose()` reuses the active engine, so new config is never hot-reloaded):

```json
{
Expand Down Expand Up @@ -108,6 +108,122 @@ Anthropic direct (Claude) — drop `baseURL`, switch `provider`:

`embedding` is optional. When present, `dimensions` must match the Neo4j vector index dimension. For a fresh database, the plugin creates matching indexes during startup. If you change dimensions later, recreate the vector indexes or the Neo4j database.

### Memory decay (forgetting curve)

Each maintenance cycle scores every active node with a three-factor weighted model (recency + frequency + intrinsic) and bidirectionally transitions nodes across three tiers: `core` / `working` / `peripheral`. Two lifecycle stages sit on top of the forgetting curve (both on by default, configurable via `decay`): **auto-deprecation** — a `peripheral` node whose composite stays below `peripheralCompositeThreshold` and which has not been accessed for `autoDeprecateAfterDays` (30) days gets its edges severed and is marked `deprecated`; if the same knowledge is extracted or edited again, the node automatically revives to `active`. **purge** — any `deprecated` node (manual deprecation and merge losers included) is hard-deleted (`DETACH DELETE`, vectors included) after `purgeAfterDays` (60) days to reclaim storage.

The full formula, field mapping from the reference implementation, default-value rationale, and tuning guide live in **[`docs/decay.md`](docs/decay.md)**.

Minimal config (all fields optional, defaults shown):

```json
"decay": { "enabled": true }
```

Common overrides — for fuller control see `docs/decay.md` §4:

```json
"decay": {
"enabled": true,
"recencyHalfLifeDays": 30,
"peripheralCompositeThreshold": 0.15,
"workingAccessThreshold": 3
}
```

### Cron sessions

Sessions created by OpenClaw scheduled tasks can be configured independently of normal sessions. The host places the cron marker on the **sessionKey** (`sessionId` is a random UUID); real shapes are `cron:<jobId>`, `agent:<agentId>:cron:<jobId>`, or `agent:<agentId>:cron:<jobId>:run:<runId>`:

```json
"cron": {
"enabled": true,
"extract": true,
"finalizeAndMaintain": true
}
```

| Option | Default | Description |
| --- | --- | --- |
| `enabled` | `true` | Enable graph functionality inside cron sessions (recall injection + message buffering). When `false`, cron sessions skip automatic recall and message persistence; the `gm_*` tools remain available for explicit calls (manual escape hatch). |
| `extract` | `true` | Trigger knowledge extraction (LLM triples) in cron sessions via `afterTurn` / `commitTurn` / `compact`. When `false`, messages are still buffered and can be backfilled later with `openclaw graph-memory extract`. |
| `finalizeAndMaintain` | `true` | Run finalize (EVENT→SKILL promotion) and graph maintenance (decay / PageRank / communities) when a cron session ends. Disable when frequent cron runs make end-of-session global maintenance too costly. |

All three options default to **`true`**: cron sessions behave like normal sessions (recall, buffering, extraction, and end-of-session maintenance all enabled) unless explicitly disabled. `enabled: false` is the master switch — even with `extract` / `finalizeAndMaintain` set to `true`, nothing runs. Non-cron sessions are never affected by these options.

All three sub-options are optional; omitted fields keep the default `true` (e.g. with `"cron": { "extract": false }` only extraction is disabled — recall, buffering, and end-of-session maintenance stay on).

Caveat: when a cron job sets an explicit custom `sessionKey`, the host does not append the `cron` segment — such sessions cannot be detected and are treated as normal sessions.

### Knowledge extraction (extract, LLM cost control)

By default every turn triggers an immediate LLM extraction (`per-turn` mode), with a local pre-filter that skips trivial turns (empty input, filler words, or ultra-short input without technical tokens — turns containing tool work are never skipped) without calling the LLM. Chatty sessions can switch to batched extraction to cut LLM call volume to ~1/N:

```json
"extract": {
"mode": "batched",
"trivialMaxChars": 5
}
```

| Option | Default | Description |
| --- | --- | --- |
| `mode` | `"per-turn"` | `per-turn` = extract immediately after each turn (`afterTurn` / `commitTurn`), so knowledge is queryable mid-session; `batched` = no per-turn LLM calls — unextracted messages accumulate and are batch-extracted once they reach `compactTurnCount * 3` messages, with the tail batch flushed at session end. Trade-off: mid-session recall cannot see the latest turns' knowledge until the next batch (or session end) runs. |
| `trivialMaxChars` | `5` | Local pre-filter threshold (`per-turn` path only): if the user text, after stripping whitespace/punctuation, is at or under this length **and** contains no technical tokens (3+ alphanumeric run, e.g. `pnpm`/`jwt`), the turn is marked extracted without an LLM call. Trivially-skipped turns are marked `producedKnowledge=false`, so retention never deletes their raw evidence. |
| `trivialPrompts` | — | Extra filler words merged with the built-in stoplist (exact match after cleaning + lowercasing, e.g. `"继续"`, `"resume"`). |

### Context compaction (delegating mode)

This engine runs in OpenClaw's **delegating mode** (`ownsCompaction: false`): `/compact`, near-limit auto-compaction, and overflow-recovery compaction all delegate window reduction to OpenClaw's built-in summarization pipeline (the same bridge the legacy engine uses, `delegateCompactionToRuntime` from `openclaw/plugin-sdk/core`). When a compaction fires, the engine additionally kicks off a **fire-and-forget** extraction of any unextracted messages (the `GmMessage` backlog — host compaction only rewrites the transcript, never the graph store), so knowledge is preserved into the graph without blocking compaction or the next user message.

`assemble()` declares `promptAuthority: "preassembly_may_overflow"`: the host keeps its generic pre-prompt overflow precheck active and takes the maximum of the assembled estimate and the unwindowed session-history estimate when deciding whether to preemptively compact — the recent-turn window alone can never hide an overflow that would fail the turn. Requires OpenClaw 2026.6+ (older hosts fall back to extraction-only compaction with a warning; the host's own in-attempt auto-compaction still runs).

### Raw message retention (messageRetention, opt-in)

Context compaction only changes what the model sees — it is **not authorization to delete persistent evidence**. The default `keep=all` never deletes any raw message (GmMessage) at zero overhead. To bound database growth, opt into bounded pruning; it runs at the tail of the graph maintenance chain and processes at most `batchSize` rows per cycle:

```json
"messageRetention": {
"keep": "referenced",
"batchSize": 500,
"dryRun": false
}
```

| Option | Default | Description |
| --- | --- | --- |
| `keep` | `"all"` | `all` = keep everything (default, zero behavior change); `referenced` = delete only messages that were extracted **and** actually produced knowledge; `recent` = `referenced` plus a time-window guard (requires at least one window option). |
| `recentTurns` | `0` | `keep=recent`: keep the newest N real user turns per session (that turn and everything after it). Sessions with no user messages are fully protected. |
| `retentionDays` | `0` | `keep=recent`: keep messages ingested within the last N days. |
| `batchSize` | `500` | Maximum rows processed per maintenance cycle (1–10000), keeping the chain bounded. |
| `dryRun` | `false` | `true` reports the candidate set without deleting — **run one `dryRun` cycle to validate candidates before enabling pruning**. |

Deletion semantics (fail-closed):

- Only messages with `extracted=true` **and** `producedKnowledge=true` (the extraction actually produced nodes/edges) enter the candidate set.
- Turns skipped by the trivial pre-filter, and turns where the LLM returned zero nodes and zero edges, are marked `producedKnowledge=false` — the raw evidence stays in the database. Such turns are never re-extracted automatically; to re-mine them with a better prompt/model, manually reset their `extracted` flag to `false` and re-run `openclaw graph-memory extract`.
- Unextracted messages and legacy rows (extracted before this flag existed, no `producedKnowledge` property) are never deleted.
- A failure in the retention step itself (invalid policy fails closed with zero deletions / Neo4j error) never invalidates the other maintenance steps; it retries next cycle.

### Re-embedding after switching embedding models

Embedding vectors are model-specific — vectors produced by the old model are not comparable
(different dimensions break dedup and vector search outright), and vectors are write-once with
no automatic migration. After changing `embedding.model` / `embedding.dimensions`, run:

```bash
openclaw graph-memory reembed --dry-run # report dimension match + vector coverage, no writes
openclaw graph-memory reembed # void all vectors and rebuild them in batches
```

The command voids every `MemoryNode.embedding` (and its `contentHash`, so the runtime
`syncEmbed` hash guard cannot short-circuit), then re-embeds all active nodes and community
summaries with the current model using batched requests (`--batch <n>`, default 32; failed
batches automatically fall back to per-item requests). If the vector index dimensions no longer
match the configured model, the run aborts — add `--recreate-index` to drop and recreate
`gm_node_embedding` / `gm_community_embedding` with the new dimensions. Items that fail stay
vectorless and are picked up by a re-run.

### OAuth login (experimental)

```bash
Expand All @@ -127,7 +243,8 @@ conversation messages -> GmMessage nodes -> LLM triple extraction
-> embeddings -> vector recall + community expansion + GDS PPR
-> XML context injection

session end -> dedup -> global PageRank -> communities -> summaries
session end -> decay (forgetting curve) -> purge expired deprecated -> dedup -> global PageRank -> communities -> summaries
-> message retention (opt-in)
```

## Verify
Expand Down Expand Up @@ -158,18 +275,18 @@ Inspect the graph with the bundled Cypher shell:
| --- | --- |
| `gm_search` | Recall graph knowledge for a query |
| `gm_record` | Add a knowledge node manually |
| `gm_update` | Update, delete, or deprecate an existing node by exact name. `mode=update` (default) refines description/content; `mode=delete` hard-deletes the node and all its relationships; `mode=deprecate` marks `[DEPRECATED]` and removes all relationships while keeping the node (throws if not found) |
| `gm_update` | Update or deprecate an existing node by exact name. `mode=update` (default) refines description/content; `mode=deprecate` marks `[DEPRECATED]`, removes all relationships, and makes the node unreachable from recall (equivalent to deletion; physically purged by maintenance after `purgeAfterDays`). `mode=delete` was removed — deprecate replaces it (throws if not found) |
| `gm_link` | Manually create or refine an edge between two existing nodes (validates type + direction against the whitelist; idempotent on from+to+type) |
| `gm_unlink` | Remove edges between two nodes by name; optional `type` filter, otherwise all from→to edges |
| `gm_merge` | Merge two same-type duplicate nodes: keep absorbs content/validatedCount/sessions + dedup-aware edge migration; merge is soft-deleted |
| `gm_stats` | Show node, relationship, community, and PageRank statistics |
| `gm_maintain` | Run deduplication, PageRank, and community maintenance |
| `gm_maintain` | Run decay scoring, deduplication, PageRank, and community maintenance |

## Development

```bash
npm install
npm run build # tsc --noEmit
npm run build # compiles to dist/
npm test # unit tests only (no Neo4j required)
```

Expand Down
Loading