openclaw-plugin-memory-engine 3.1.1 → 4.0.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/assets/icon.png +0 -0
- package/dist/admin.d.ts +12 -0
- package/dist/admin.js +217 -0
- package/dist/control-ui/14e2bd207bb33b0189bdfc2712c6eee0f901d10bca0f92cb7773a4bd0f93e10b/index.js +7 -0
- package/dist/control-ui/65180acf7c9a7bc2632152ce986b4162c1efb288bab4ebf6e0d0668edbbe2b75/index.js +7 -0
- package/dist/control-ui.d.ts +2 -0
- package/dist/control-ui.js +315 -0
- package/dist/curator.d.ts +2 -2
- package/dist/entry.js +20 -11
- package/dist/index.d.ts +14 -1
- package/dist/index.js +13 -1
- package/dist/maintenance.d.ts +12 -0
- package/dist/maintenance.js +192 -0
- package/dist/semantic.d.ts +13 -0
- package/dist/semantic.js +91 -0
- package/dist/tools.d.ts +3 -3
- package/dist/tools.js +15 -2
- package/dist/ui-contract.d.ts +135 -0
- package/dist/ui-contract.js +80 -0
- package/dist/ui-feature.d.ts +79 -0
- package/dist/ui-feature.js +110 -0
- package/openclaw.plugin.json +34 -5
- package/package.json +8 -6
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { defineFeatureContract } from "openclaw/plugin-sdk/feature-contract";
|
|
3
|
+
export const contract = defineFeatureContract({
|
|
4
|
+
pluginId: "memory-engine",
|
|
5
|
+
operations: {
|
|
6
|
+
status: {
|
|
7
|
+
kind: "query",
|
|
8
|
+
description: "Cognitive health metrics for the memory graph.",
|
|
9
|
+
input: Type.Object({}, { additionalProperties: false }),
|
|
10
|
+
output: Type.Object({ atoms_active: Type.Integer(), durable_atoms: Type.Integer(), bonds: Type.Integer(), isolated: Type.Integer(), pending: Type.Integer(), missing_compact: Type.Integer(), by_type: Type.Record(Type.String(), Type.Integer()), recommendations: Type.Array(Type.String()) }, { additionalProperties: true }),
|
|
11
|
+
},
|
|
12
|
+
search: {
|
|
13
|
+
kind: "query",
|
|
14
|
+
description: "FTS search over memory atoms with optional type/domain filters.",
|
|
15
|
+
input: Type.Object({ query: Type.String({ maxLength: 500, minLength: 1 }), limit: Type.Optional(Type.Integer({ maximum: 100 })), type: Type.Optional(Type.String({ maxLength: 40 })), domain: Type.Optional(Type.String({ maxLength: 200 })) }, { additionalProperties: false }),
|
|
16
|
+
output: Type.Object({ results: Type.Array(Type.Object({ id: Type.String(), title: Type.String(), domain: Type.Optional(Type.String()), type: Type.Optional(Type.String()) })) }, { additionalProperties: true }),
|
|
17
|
+
},
|
|
18
|
+
atom: {
|
|
19
|
+
kind: "query",
|
|
20
|
+
description: "Full atom detail with bonds.",
|
|
21
|
+
input: Type.Object({ id: Type.String({ maxLength: 200 }) }, { additionalProperties: false }),
|
|
22
|
+
output: Type.Object({}, { additionalProperties: true }),
|
|
23
|
+
},
|
|
24
|
+
domains: {
|
|
25
|
+
kind: "query",
|
|
26
|
+
description: "List distinct domains with atom counts.",
|
|
27
|
+
input: Type.Object({}, { additionalProperties: false }),
|
|
28
|
+
output: Type.Object({ domains: Type.Array(Type.Object({ domain: Type.String(), count: Type.Integer() })) }, { additionalProperties: true }),
|
|
29
|
+
},
|
|
30
|
+
curator_dry: {
|
|
31
|
+
kind: "action",
|
|
32
|
+
description: "Run curator in dry-run mode.",
|
|
33
|
+
input: Type.Object({ max_atoms: Type.Optional(Type.Integer({ maximum: 200 })) }, { additionalProperties: false }),
|
|
34
|
+
output: Type.Object({}, { additionalProperties: true }),
|
|
35
|
+
},
|
|
36
|
+
curator_apply: {
|
|
37
|
+
kind: "action",
|
|
38
|
+
description: "Run curator with auto_apply (safe actions only).",
|
|
39
|
+
input: Type.Object({ max_atoms: Type.Optional(Type.Integer({ maximum: 200 })) }, { additionalProperties: false }),
|
|
40
|
+
output: Type.Object({}, { additionalProperties: true }),
|
|
41
|
+
},
|
|
42
|
+
backups_list: {
|
|
43
|
+
kind: "query",
|
|
44
|
+
description: "List available backups.",
|
|
45
|
+
input: Type.Object({}, { additionalProperties: false }),
|
|
46
|
+
output: Type.Object({ backups: Type.Array(Type.Object({ file: Type.String(), size_bytes: Type.Integer(), created: Type.String() })) }, { additionalProperties: true }),
|
|
47
|
+
},
|
|
48
|
+
backup_now: {
|
|
49
|
+
kind: "action",
|
|
50
|
+
description: "Create a database backup now.",
|
|
51
|
+
input: Type.Object({}, { additionalProperties: false }),
|
|
52
|
+
output: Type.Object({}, { additionalProperties: true }),
|
|
53
|
+
},
|
|
54
|
+
questions_list: {
|
|
55
|
+
kind: "query",
|
|
56
|
+
description: "List pending human questions.",
|
|
57
|
+
input: Type.Object({ limit: Type.Optional(Type.Integer({ maximum: 50 })) }, { additionalProperties: false }),
|
|
58
|
+
output: Type.Object({ questions: Type.Array(Type.Object({ id: Type.String(), question: Type.String(), question_type: Type.Optional(Type.String()), status: Type.String() })) }, { additionalProperties: true }),
|
|
59
|
+
},
|
|
60
|
+
question_answer: {
|
|
61
|
+
kind: "action",
|
|
62
|
+
description: "Answer a pending question (resolve, optionally archive atom).",
|
|
63
|
+
input: Type.Object({ question_id: Type.String({ maxLength: 200 }), answer: Type.String({ maxLength: 2000 }), action: Type.Optional(Type.String({ maxLength: 40 })) }, { additionalProperties: false }),
|
|
64
|
+
output: Type.Object({}, { additionalProperties: true }),
|
|
65
|
+
},
|
|
66
|
+
atom_soft_delete: {
|
|
67
|
+
kind: "action",
|
|
68
|
+
description: "Soft-delete an atom (status='deleted', reversible).",
|
|
69
|
+
input: Type.Object({ id: Type.String({ maxLength: 200 }) }, { additionalProperties: false }),
|
|
70
|
+
output: Type.Object({}, { additionalProperties: true }),
|
|
71
|
+
},
|
|
72
|
+
atom_update_domain: {
|
|
73
|
+
kind: "action",
|
|
74
|
+
description: "Move an atom to a different domain.",
|
|
75
|
+
input: Type.Object({ id: Type.String({ maxLength: 200 }), new_domain: Type.String({ maxLength: 200 }) }, { additionalProperties: false }),
|
|
76
|
+
output: Type.Object({}, { additionalProperties: true }),
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
events: {},
|
|
80
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export declare function createFeatureHandlers(): {
|
|
2
|
+
status: () => {
|
|
3
|
+
atoms_active: any;
|
|
4
|
+
durable_atoms: any;
|
|
5
|
+
bonds: any;
|
|
6
|
+
isolated: any;
|
|
7
|
+
pending: any;
|
|
8
|
+
missing_compact: any;
|
|
9
|
+
by_type: any;
|
|
10
|
+
recommendations: any;
|
|
11
|
+
};
|
|
12
|
+
search: ({ query, limit, type, domain }: any) => {
|
|
13
|
+
results: any[];
|
|
14
|
+
};
|
|
15
|
+
atom: ({ id }: any) => {
|
|
16
|
+
atom?: undefined;
|
|
17
|
+
bonds?: undefined;
|
|
18
|
+
error: string;
|
|
19
|
+
} | {
|
|
20
|
+
error?: undefined;
|
|
21
|
+
atom: any;
|
|
22
|
+
bonds: any[];
|
|
23
|
+
};
|
|
24
|
+
domains: () => {
|
|
25
|
+
domains: any[];
|
|
26
|
+
};
|
|
27
|
+
curator_dry: ({ max_atoms }: any) => {
|
|
28
|
+
compact: any;
|
|
29
|
+
bonds_suggested: any;
|
|
30
|
+
bonds_applied: any;
|
|
31
|
+
isolated_classified: number;
|
|
32
|
+
promotions: any;
|
|
33
|
+
merges: any;
|
|
34
|
+
status_after: any;
|
|
35
|
+
};
|
|
36
|
+
curator_apply: ({ max_atoms }: any) => {
|
|
37
|
+
compact: any;
|
|
38
|
+
bonds_suggested: any;
|
|
39
|
+
bonds_applied: any;
|
|
40
|
+
isolated_classified: number;
|
|
41
|
+
promotions: any;
|
|
42
|
+
merges: any;
|
|
43
|
+
status_after: any;
|
|
44
|
+
};
|
|
45
|
+
backups_list: () => {
|
|
46
|
+
backups: {
|
|
47
|
+
file: string;
|
|
48
|
+
size_bytes: number;
|
|
49
|
+
created: string;
|
|
50
|
+
}[];
|
|
51
|
+
};
|
|
52
|
+
backup_now: () => {
|
|
53
|
+
reason?: undefined;
|
|
54
|
+
ok: boolean;
|
|
55
|
+
path: string;
|
|
56
|
+
size_bytes: number;
|
|
57
|
+
} | {
|
|
58
|
+
path?: undefined;
|
|
59
|
+
size_bytes?: undefined;
|
|
60
|
+
ok: boolean;
|
|
61
|
+
reason: string;
|
|
62
|
+
};
|
|
63
|
+
questions_list: ({ limit }: any) => {
|
|
64
|
+
questions: any[];
|
|
65
|
+
};
|
|
66
|
+
question_answer: ({ question_id, answer, action }: any) => {
|
|
67
|
+
ok: boolean;
|
|
68
|
+
reason: string;
|
|
69
|
+
} | {
|
|
70
|
+
reason?: undefined;
|
|
71
|
+
ok: boolean;
|
|
72
|
+
};
|
|
73
|
+
atom_soft_delete: ({ id }: any) => {
|
|
74
|
+
ok: boolean;
|
|
75
|
+
};
|
|
76
|
+
atom_update_domain: ({ id, new_domain }: any) => {
|
|
77
|
+
ok: boolean;
|
|
78
|
+
};
|
|
79
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import { Curator } from "./curator.js";
|
|
3
|
+
import { Engine } from "./engine.js";
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
export function createFeatureHandlers() {
|
|
7
|
+
const dbPath = process.env.MEMORY_DB_PATH ?? "/data/memory.db";
|
|
8
|
+
const db = new DatabaseSync(dbPath);
|
|
9
|
+
const engine = new Engine(db, {});
|
|
10
|
+
const curator = new Curator(db, engine, {});
|
|
11
|
+
const q = (sql, ...p) => db.prepare(sql).all(...p);
|
|
12
|
+
const q1 = (sql, ...p) => db.prepare(sql).get(...p);
|
|
13
|
+
const x = (sql, ...p) => db.prepare(sql).run(...p);
|
|
14
|
+
const curatorReport = (report) => ({
|
|
15
|
+
compact: report.actions.filter((a) => a.kind === "compact").length,
|
|
16
|
+
bonds_suggested: report.actions.find((a) => a.kind === "bond_pass")?.suggestions ?? 0,
|
|
17
|
+
bonds_applied: report.actions.find((a) => a.kind === "bond_pass")?.bonds_applied ?? 0,
|
|
18
|
+
isolated_classified: (() => { const iso = report.actions.find((a) => a.kind === "isolated_classification"); return iso ? Number(Object.values(iso.counts).reduce((n, v) => Number(n) + Number(v), 0)) : 0; })(),
|
|
19
|
+
promotions: report.actions.filter((a) => a.kind === "promotion_candidate").length,
|
|
20
|
+
merges: report.actions.filter((a) => a.kind === "merge_candidate").length,
|
|
21
|
+
status_after: report.status_after,
|
|
22
|
+
});
|
|
23
|
+
return {
|
|
24
|
+
status: () => {
|
|
25
|
+
const st = curator.status();
|
|
26
|
+
return { atoms_active: st.atoms_active, durable_atoms: st.durable_atoms, bonds: st.bonds, isolated: st.isolated_durable_atoms, pending: st.pending_questions, missing_compact: st.long_atoms_missing_compact, by_type: st.by_type, recommendations: st.recommendations };
|
|
27
|
+
},
|
|
28
|
+
search: ({ query, limit, type, domain }) => {
|
|
29
|
+
const clean = String(query).replace(/[^a-zA-Z0-9àèéìòù ]/g, " ").trim();
|
|
30
|
+
if (!clean)
|
|
31
|
+
return { results: [] };
|
|
32
|
+
const words = clean.split(/\s+/).filter((w) => w.length > 1).map((w) => '"' + w + '"');
|
|
33
|
+
if (!words.length)
|
|
34
|
+
return { results: [] };
|
|
35
|
+
let rows = [];
|
|
36
|
+
try {
|
|
37
|
+
rows = db.prepare("SELECT a.id, a.title, a.domain, a.type FROM atoms_fts JOIN atoms a ON atoms_fts.rowid = a.rowid WHERE atoms_fts MATCH ? AND a.status='active' LIMIT 200").all(words.join(" OR "));
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
rows = [];
|
|
41
|
+
}
|
|
42
|
+
if (type)
|
|
43
|
+
rows = rows.filter((r) => r.type === type);
|
|
44
|
+
if (domain)
|
|
45
|
+
rows = rows.filter((r) => String(r.domain || "").startsWith(domain));
|
|
46
|
+
return { results: rows.slice(0, limit ?? 30) };
|
|
47
|
+
},
|
|
48
|
+
atom: ({ id }) => {
|
|
49
|
+
const atom = q1("SELECT * FROM atoms WHERE id=?", id);
|
|
50
|
+
if (!atom)
|
|
51
|
+
return { error: "not found" };
|
|
52
|
+
const bonds = q("SELECT * FROM bonds WHERE from_id=? OR to_id=?", id, id);
|
|
53
|
+
return { atom, bonds };
|
|
54
|
+
},
|
|
55
|
+
domains: () => ({
|
|
56
|
+
domains: q("SELECT domain, COUNT(*) as count FROM atoms WHERE status='active' GROUP BY domain ORDER BY count DESC LIMIT 50"),
|
|
57
|
+
}),
|
|
58
|
+
curator_dry: ({ max_atoms }) => curatorReport(curator.run(true, false, max_atoms ?? 40)),
|
|
59
|
+
curator_apply: ({ max_atoms }) => curatorReport(curator.run(false, true, max_atoms ?? 40)),
|
|
60
|
+
backups_list: () => {
|
|
61
|
+
const backupDir = path.join(path.dirname(dbPath), "backups");
|
|
62
|
+
if (!fs.existsSync(backupDir))
|
|
63
|
+
return { backups: [] };
|
|
64
|
+
const backups = fs.readdirSync(backupDir).filter((f) => f.endsWith(".db")).map((f) => {
|
|
65
|
+
const st = fs.statSync(path.join(backupDir, f));
|
|
66
|
+
return { file: f, size_bytes: st.size, created: st.mtime.toISOString() };
|
|
67
|
+
}).sort((a, b) => b.created.localeCompare(a.created)).slice(0, 20);
|
|
68
|
+
return { backups };
|
|
69
|
+
},
|
|
70
|
+
backup_now: () => {
|
|
71
|
+
try {
|
|
72
|
+
const backupDir = path.join(path.dirname(dbPath), "backups");
|
|
73
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
74
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
75
|
+
const dest = path.join(backupDir, "memory-backup-" + ts + ".db");
|
|
76
|
+
db.prepare("VACUUM INTO ?").run(dest);
|
|
77
|
+
return { ok: true, path: dest, size_bytes: fs.statSync(dest).size };
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
return { ok: false, reason: String(e) };
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
questions_list: ({ limit }) => ({
|
|
84
|
+
questions: q("SELECT id, question, question_type, status, atom_ids FROM human_questions WHERE status='pending' ORDER BY created_at DESC LIMIT ?", limit ?? 20),
|
|
85
|
+
}),
|
|
86
|
+
question_answer: ({ question_id, answer, action }) => {
|
|
87
|
+
const row = q1("SELECT * FROM human_questions WHERE id=? AND status='pending'", question_id);
|
|
88
|
+
if (!row)
|
|
89
|
+
return { ok: false, reason: "question not found or not pending" };
|
|
90
|
+
x("UPDATE human_questions SET status='answered', answer=?, answered_at=unixepoch() WHERE id=?", answer, question_id);
|
|
91
|
+
if (action === "archive_atom" && row.atom_ids) {
|
|
92
|
+
try {
|
|
93
|
+
const ids = JSON.parse(row.atom_ids);
|
|
94
|
+
for (const aid of ids)
|
|
95
|
+
x("UPDATE atoms SET status='archived', updated_at=unixepoch() WHERE id=?", aid);
|
|
96
|
+
}
|
|
97
|
+
catch { }
|
|
98
|
+
}
|
|
99
|
+
return { ok: true };
|
|
100
|
+
},
|
|
101
|
+
atom_soft_delete: ({ id }) => {
|
|
102
|
+
const r = x("UPDATE atoms SET status='deleted', updated_at=unixepoch() WHERE id=? AND status='active'", id);
|
|
103
|
+
return { ok: r.changes > 0 };
|
|
104
|
+
},
|
|
105
|
+
atom_update_domain: ({ id, new_domain }) => {
|
|
106
|
+
const r = x("UPDATE atoms SET domain=?, updated_at=unixepoch() WHERE id=?", new_domain, id);
|
|
107
|
+
return { ok: r.changes > 0 };
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "memory-engine",
|
|
3
3
|
"name": "Memory Engine",
|
|
4
|
-
"description": "Native Memory Engine:
|
|
5
|
-
"version": "
|
|
4
|
+
"description": "Native Memory Engine: 45 memory tools, session ingest/digest, curator and Control UI.",
|
|
5
|
+
"version": "4.0.0",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
8
|
-
"
|
|
9
|
-
"
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {}
|
|
10
10
|
},
|
|
11
11
|
"activation": {
|
|
12
12
|
"onStartup": true
|
|
13
13
|
},
|
|
14
|
+
"controlUi": {
|
|
15
|
+
"entry": "dist/control-ui/14e2bd207bb33b0189bdfc2712c6eee0f901d10bca0f92cb7773a4bd0f93e10b/index.js"
|
|
16
|
+
},
|
|
14
17
|
"contracts": {
|
|
15
18
|
"tools": [
|
|
16
19
|
"memory_version",
|
|
@@ -31,7 +34,33 @@
|
|
|
31
34
|
"memory_generate_session_digest",
|
|
32
35
|
"memory_admin",
|
|
33
36
|
"memory_curator_run",
|
|
34
|
-
"memory_cognitive_status"
|
|
37
|
+
"memory_cognitive_status",
|
|
38
|
+
"memory_summary",
|
|
39
|
+
"memory_delete_atom",
|
|
40
|
+
"memory_unlink",
|
|
41
|
+
"memory_error_list",
|
|
42
|
+
"memory_list_contradictions",
|
|
43
|
+
"memory_decay_run",
|
|
44
|
+
"memory_update_domain",
|
|
45
|
+
"memory_classify_memory_tier",
|
|
46
|
+
"memory_cleanup_sessions",
|
|
47
|
+
"memory_find_similar",
|
|
48
|
+
"memory_merge_atoms",
|
|
49
|
+
"memory_cleanup_duplicates",
|
|
50
|
+
"memory_semantic_search",
|
|
51
|
+
"memory_reindex_embeddings",
|
|
52
|
+
"memory_suggest_bonds",
|
|
53
|
+
"memory_suggest_bonds_all",
|
|
54
|
+
"memory_backup_database",
|
|
55
|
+
"memory_restore_database",
|
|
56
|
+
"memory_list_backups",
|
|
57
|
+
"memory_export_all",
|
|
58
|
+
"memory_export_atom",
|
|
59
|
+
"memory_import_data",
|
|
60
|
+
"memory_import_markdown",
|
|
61
|
+
"memory_learning_run",
|
|
62
|
+
"memory_ask_pending",
|
|
63
|
+
"memory_answer_human"
|
|
35
64
|
]
|
|
36
65
|
}
|
|
37
66
|
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-plugin-memory-engine",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
|
-
"build": "tsc -p tsconfig.json",
|
|
6
|
+
"build": "tsc -p tsconfig.json && openclaw plugins build",
|
|
7
7
|
"plugin:build": "npm run build && openclaw plugins build --entry ./dist/index.js",
|
|
8
8
|
"plugin:validate": "npm run build && openclaw plugins validate --entry ./dist/index.js",
|
|
9
9
|
"test": "vitest run --config ./vitest.config.ts"
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
|
+
"README.md",
|
|
13
|
+
"assets",
|
|
12
14
|
"dist",
|
|
13
|
-
"openclaw.plugin.json"
|
|
14
|
-
"README.md"
|
|
15
|
+
"openclaw.plugin.json"
|
|
15
16
|
],
|
|
16
17
|
"peerDependencies": {
|
|
17
18
|
"openclaw": ">=2026.5.17"
|
|
@@ -22,7 +23,7 @@
|
|
|
22
23
|
"devDependencies": {
|
|
23
24
|
"openclaw": "latest",
|
|
24
25
|
"typescript": "^7.0.2",
|
|
25
|
-
"vitest": "^3.
|
|
26
|
+
"vitest": "^3.3.0"
|
|
26
27
|
},
|
|
27
28
|
"openclaw": {
|
|
28
29
|
"extensions": [
|
|
@@ -33,7 +34,8 @@
|
|
|
33
34
|
},
|
|
34
35
|
"build": {
|
|
35
36
|
"openclawVersion": "2026.9.7"
|
|
36
|
-
}
|
|
37
|
+
},
|
|
38
|
+
"controlUi": "./src/control-ui.ts"
|
|
37
39
|
},
|
|
38
40
|
"description": "Native Memory Engine for OpenClaw: 19 memory tools + session ingest/digest + curator",
|
|
39
41
|
"license": "MIT",
|