@memtensor/memos-local-openclaw-plugin 1.0.8-beta → 1.0.8-beta.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.
- package/index.ts +5 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/storage/sqlite.ts +140 -4
- package/src/viewer/html.ts +579 -212
- package/src/viewer/server.ts +177 -8
package/index.ts
CHANGED
|
@@ -1151,6 +1151,10 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1151
1151
|
};
|
|
1152
1152
|
}
|
|
1153
1153
|
|
|
1154
|
+
const disabledWarning = skill.status === "archived"
|
|
1155
|
+
? "\n\n> **Warning:** This skill is currently **disabled** (archived). Its content is shown for reference only — it will not be used in search or auto-recall.\n\n"
|
|
1156
|
+
: "";
|
|
1157
|
+
|
|
1154
1158
|
const manifest = skillInstaller.getCompanionManifest(resolvedSkillId);
|
|
1155
1159
|
let footer = "\n\n---\n";
|
|
1156
1160
|
|
|
@@ -1175,7 +1179,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1175
1179
|
return {
|
|
1176
1180
|
content: [{
|
|
1177
1181
|
type: "text",
|
|
1178
|
-
text: `## Skill: ${skill.name} (v${skill.version})\n\n${sv.content}${footer}`,
|
|
1182
|
+
text: `## Skill: ${skill.name} (v${skill.version})${disabledWarning}\n\n${sv.content}${footer}`,
|
|
1179
1183
|
}],
|
|
1180
1184
|
details: {
|
|
1181
1185
|
skillId: skill.id,
|
package/openclaw.plugin.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "MemOS Local Memory",
|
|
4
4
|
"description": "Full-write local conversation memory with hybrid search (RRF + MMR + recency), task summarization, skill evolution, and team sharing (Hub-Client). Provides memory_search, memory_get, task_summary, skill_search, task_share, network_skill_pull, network_team_info, memory_viewer for layered retrieval and team collaboration.",
|
|
5
5
|
"kind": "memory",
|
|
6
|
-
"version": "1.0.
|
|
6
|
+
"version": "1.0.8-beta.2",
|
|
7
7
|
"skills": [
|
|
8
8
|
"skill/memos-memory-guide"
|
|
9
9
|
],
|
package/package.json
CHANGED
package/src/storage/sqlite.ts
CHANGED
|
@@ -110,6 +110,7 @@ export class SqliteStore {
|
|
|
110
110
|
this.migrateOwnerFields();
|
|
111
111
|
this.migrateSkillVisibility();
|
|
112
112
|
this.migrateSkillEmbeddingsAndFts();
|
|
113
|
+
this.migrateTaskEmbeddingsAndFts();
|
|
113
114
|
this.migrateFtsToTrigram();
|
|
114
115
|
this.migrateHubTables();
|
|
115
116
|
this.migrateHubFtsToTrigram();
|
|
@@ -290,6 +291,55 @@ export class SqliteStore {
|
|
|
290
291
|
} catch { /* best-effort */ }
|
|
291
292
|
}
|
|
292
293
|
|
|
294
|
+
private migrateTaskEmbeddingsAndFts(): void {
|
|
295
|
+
this.db.exec(`
|
|
296
|
+
CREATE TABLE IF NOT EXISTS task_embeddings (
|
|
297
|
+
task_id TEXT PRIMARY KEY REFERENCES tasks(id) ON DELETE CASCADE,
|
|
298
|
+
vector BLOB NOT NULL,
|
|
299
|
+
dimensions INTEGER NOT NULL,
|
|
300
|
+
updated_at INTEGER NOT NULL
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS tasks_fts USING fts5(
|
|
304
|
+
summary,
|
|
305
|
+
topic,
|
|
306
|
+
content='tasks',
|
|
307
|
+
content_rowid='rowid',
|
|
308
|
+
tokenize='trigram'
|
|
309
|
+
);
|
|
310
|
+
`);
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
this.db.exec(`
|
|
314
|
+
CREATE TRIGGER IF NOT EXISTS tasks_fts_ai AFTER INSERT ON tasks BEGIN
|
|
315
|
+
INSERT INTO tasks_fts(rowid, summary, topic)
|
|
316
|
+
VALUES (new.rowid, new.summary, COALESCE(new.topic, ''));
|
|
317
|
+
END;
|
|
318
|
+
CREATE TRIGGER IF NOT EXISTS tasks_fts_ad AFTER DELETE ON tasks BEGIN
|
|
319
|
+
INSERT INTO tasks_fts(tasks_fts, rowid, summary, topic)
|
|
320
|
+
VALUES ('delete', old.rowid, old.summary, COALESCE(old.topic, ''));
|
|
321
|
+
END;
|
|
322
|
+
CREATE TRIGGER IF NOT EXISTS tasks_fts_au AFTER UPDATE ON tasks BEGIN
|
|
323
|
+
INSERT INTO tasks_fts(tasks_fts, rowid, summary, topic)
|
|
324
|
+
VALUES ('delete', old.rowid, old.summary, COALESCE(old.topic, ''));
|
|
325
|
+
INSERT INTO tasks_fts(rowid, summary, topic)
|
|
326
|
+
VALUES (new.rowid, new.summary, COALESCE(new.topic, ''));
|
|
327
|
+
END;
|
|
328
|
+
`);
|
|
329
|
+
} catch {
|
|
330
|
+
// triggers may already exist
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
const count = (this.db.prepare("SELECT COUNT(*) as c FROM tasks_fts").get() as { c: number }).c;
|
|
335
|
+
const taskCount = (this.db.prepare("SELECT COUNT(*) as c FROM tasks").get() as { c: number }).c;
|
|
336
|
+
if (count === 0 && taskCount > 0) {
|
|
337
|
+
this.db.exec("INSERT INTO tasks_fts(rowid, summary, topic) SELECT rowid, summary, COALESCE(topic, '') FROM tasks");
|
|
338
|
+
this.log.info(`Migrated: backfilled tasks_fts for ${taskCount} tasks`);
|
|
339
|
+
}
|
|
340
|
+
} catch { /* best-effort */ }
|
|
341
|
+
}
|
|
342
|
+
|
|
293
343
|
private migrateFtsToTrigram(): void {
|
|
294
344
|
// Check if chunks_fts still uses the old tokenizer (porter unicode61)
|
|
295
345
|
try {
|
|
@@ -1477,6 +1527,21 @@ export class SqliteStore {
|
|
|
1477
1527
|
return result.changes > 0;
|
|
1478
1528
|
}
|
|
1479
1529
|
|
|
1530
|
+
disableSkill(skillId: string): boolean {
|
|
1531
|
+
const skill = this.getSkill(skillId);
|
|
1532
|
+
if (!skill || skill.status === "archived") return false;
|
|
1533
|
+
this.db.prepare("DELETE FROM skill_embeddings WHERE skill_id = ?").run(skillId);
|
|
1534
|
+
this.updateSkill(skillId, { status: "archived", installed: 0 });
|
|
1535
|
+
return true;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
enableSkill(skillId: string): boolean {
|
|
1539
|
+
const skill = this.getSkill(skillId);
|
|
1540
|
+
if (!skill || skill.status !== "archived") return false;
|
|
1541
|
+
this.updateSkill(skillId, { status: "active" });
|
|
1542
|
+
return true;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1480
1545
|
// ─── Task CRUD ───
|
|
1481
1546
|
|
|
1482
1547
|
insertTask(task: Task): void {
|
|
@@ -1558,10 +1623,11 @@ export class SqliteStore {
|
|
|
1558
1623
|
return rows.map(rowToChunk);
|
|
1559
1624
|
}
|
|
1560
1625
|
|
|
1561
|
-
listTasks(opts: { status?: string; limit?: number; offset?: number; owner?: string } = {}): { tasks: Task[]; total: number } {
|
|
1626
|
+
listTasks(opts: { status?: string; limit?: number; offset?: number; owner?: string; session?: string } = {}): { tasks: Task[]; total: number } {
|
|
1562
1627
|
const conditions: string[] = [];
|
|
1563
1628
|
const params: unknown[] = [];
|
|
1564
1629
|
if (opts.status) { conditions.push("status = ?"); params.push(opts.status); }
|
|
1630
|
+
if (opts.session) { conditions.push("session_key = ?"); params.push(opts.session); }
|
|
1565
1631
|
if (opts.owner) {
|
|
1566
1632
|
conditions.push("(owner = ? OR (owner = 'public' AND id IN (SELECT task_id FROM local_shared_tasks WHERE original_owner = ?)))");
|
|
1567
1633
|
params.push(opts.owner, opts.owner);
|
|
@@ -1683,9 +1749,24 @@ export class SqliteStore {
|
|
|
1683
1749
|
this.db.prepare(`UPDATE skills SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
1684
1750
|
}
|
|
1685
1751
|
|
|
1686
|
-
listSkills(opts: { status?: string } = {}): Skill[] {
|
|
1687
|
-
const
|
|
1688
|
-
const params
|
|
1752
|
+
listSkills(opts: { status?: string; session?: string; owner?: string } = {}): Skill[] {
|
|
1753
|
+
const conditions: string[] = [];
|
|
1754
|
+
const params: unknown[] = [];
|
|
1755
|
+
if (opts.status) { conditions.push("status = ?"); params.push(opts.status); }
|
|
1756
|
+
if (opts.owner) {
|
|
1757
|
+
conditions.push("(owner = ? OR owner = 'public')");
|
|
1758
|
+
params.push(opts.owner);
|
|
1759
|
+
}
|
|
1760
|
+
if (opts.session) {
|
|
1761
|
+
conditions.push(`EXISTS (
|
|
1762
|
+
SELECT 1
|
|
1763
|
+
FROM task_skills ts
|
|
1764
|
+
JOIN tasks t ON t.id = ts.task_id
|
|
1765
|
+
WHERE ts.skill_id = skills.id AND t.session_key = ?
|
|
1766
|
+
)`);
|
|
1767
|
+
params.push(opts.session);
|
|
1768
|
+
}
|
|
1769
|
+
const cond = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1689
1770
|
const rows = this.db.prepare(`SELECT * FROM skills ${cond} ORDER BY updated_at DESC`).all(...params) as SkillRow[];
|
|
1690
1771
|
return rows.map(rowToSkill);
|
|
1691
1772
|
}
|
|
@@ -1775,6 +1856,61 @@ export class SqliteStore {
|
|
|
1775
1856
|
}
|
|
1776
1857
|
}
|
|
1777
1858
|
|
|
1859
|
+
// ─── Task Embeddings & Search ───
|
|
1860
|
+
|
|
1861
|
+
upsertTaskEmbedding(taskId: string, vector: number[]): void {
|
|
1862
|
+
const buf = Buffer.from(new Float32Array(vector).buffer);
|
|
1863
|
+
this.db.prepare(`
|
|
1864
|
+
INSERT OR REPLACE INTO task_embeddings (task_id, vector, dimensions, updated_at)
|
|
1865
|
+
VALUES (?, ?, ?, ?)
|
|
1866
|
+
`).run(taskId, buf, vector.length, Date.now());
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
getTaskEmbeddings(owner?: string): Array<{ taskId: string; vector: number[] }> {
|
|
1870
|
+
let sql = `SELECT te.task_id, te.vector, te.dimensions
|
|
1871
|
+
FROM task_embeddings te
|
|
1872
|
+
JOIN tasks t ON t.id = te.task_id`;
|
|
1873
|
+
const params: any[] = [];
|
|
1874
|
+
if (owner) {
|
|
1875
|
+
sql += ` WHERE (t.owner = ? OR t.owner = 'public')`;
|
|
1876
|
+
params.push(owner);
|
|
1877
|
+
}
|
|
1878
|
+
const rows = this.db.prepare(sql).all(...params) as Array<{ task_id: string; vector: Buffer; dimensions: number }>;
|
|
1879
|
+
return rows.map((r) => ({
|
|
1880
|
+
taskId: r.task_id,
|
|
1881
|
+
vector: Array.from(new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions)),
|
|
1882
|
+
}));
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
taskFtsSearch(query: string, limit: number, owner?: string): Array<{ taskId: string; score: number }> {
|
|
1886
|
+
const sanitized = sanitizeFtsQuery(query);
|
|
1887
|
+
if (!sanitized) return [];
|
|
1888
|
+
try {
|
|
1889
|
+
let sql = `
|
|
1890
|
+
SELECT t.id as task_id, rank
|
|
1891
|
+
FROM tasks_fts f
|
|
1892
|
+
JOIN tasks t ON t.rowid = f.rowid
|
|
1893
|
+
WHERE tasks_fts MATCH ?`;
|
|
1894
|
+
const params: any[] = [sanitized];
|
|
1895
|
+
if (owner) {
|
|
1896
|
+
sql += ` AND (t.owner = ? OR t.owner = 'public')`;
|
|
1897
|
+
params.push(owner);
|
|
1898
|
+
}
|
|
1899
|
+
sql += ` ORDER BY rank LIMIT ?`;
|
|
1900
|
+
params.push(limit);
|
|
1901
|
+
const rows = this.db.prepare(sql).all(...params) as Array<{ task_id: string; rank: number }>;
|
|
1902
|
+
if (rows.length === 0) return [];
|
|
1903
|
+
const maxAbsRank = Math.max(...rows.map((r) => Math.abs(r.rank)));
|
|
1904
|
+
return rows.map((r) => ({
|
|
1905
|
+
taskId: r.task_id,
|
|
1906
|
+
score: maxAbsRank > 0 ? Math.abs(r.rank) / maxAbsRank : 0,
|
|
1907
|
+
}));
|
|
1908
|
+
} catch {
|
|
1909
|
+
this.log.warn(`Task FTS query failed for: "${sanitized}", returning empty`);
|
|
1910
|
+
return [];
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1778
1914
|
listPublicSkills(): Skill[] {
|
|
1779
1915
|
const rows = this.db.prepare("SELECT * FROM skills WHERE visibility = 'public' AND status = 'active' ORDER BY updated_at DESC").all() as SkillRow[];
|
|
1780
1916
|
return rows.map(rowToSkill);
|