@memtensor/memos-local-openclaw-plugin 1.0.7 → 1.0.8-beta.10

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.
@@ -110,9 +110,12 @@ export class SqliteStore {
110
110
  this.migrateOwnerFields();
111
111
  this.migrateSkillVisibility();
112
112
  this.migrateSkillEmbeddingsAndFts();
113
+ this.migrateTaskTopicColumn();
114
+ this.migrateTaskEmbeddingsAndFts();
113
115
  this.migrateFtsToTrigram();
114
116
  this.migrateHubTables();
115
117
  this.migrateHubFtsToTrigram();
118
+ this.migrateHubMemorySourceAgent();
116
119
  this.migrateLocalSharedTasksOwner();
117
120
  this.migrateHubUserIdentityFields();
118
121
  this.migrateClientHubConnectionIdentityFields();
@@ -124,6 +127,16 @@ export class SqliteStore {
124
127
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_dedup_created ON chunks(dedup_status, created_at DESC)");
125
128
  }
126
129
 
130
+ private migrateHubMemorySourceAgent(): void {
131
+ try {
132
+ const cols = this.db.prepare("PRAGMA table_info(hub_memories)").all() as Array<{ name: string }>;
133
+ if (cols.length > 0 && !cols.some((c) => c.name === "source_agent")) {
134
+ this.db.exec("ALTER TABLE hub_memories ADD COLUMN source_agent TEXT NOT NULL DEFAULT ''");
135
+ this.log.info("Migrated: added source_agent column to hub_memories");
136
+ }
137
+ } catch { /* table may not exist yet */ }
138
+ }
139
+
127
140
  private migrateLocalSharedTasksOwner(): void {
128
141
  try {
129
142
  const cols = this.db.prepare("PRAGMA table_info(local_shared_tasks)").all() as Array<{ name: string }>;
@@ -290,6 +303,55 @@ export class SqliteStore {
290
303
  } catch { /* best-effort */ }
291
304
  }
292
305
 
306
+ private migrateTaskEmbeddingsAndFts(): void {
307
+ this.db.exec(`
308
+ CREATE TABLE IF NOT EXISTS task_embeddings (
309
+ task_id TEXT PRIMARY KEY REFERENCES tasks(id) ON DELETE CASCADE,
310
+ vector BLOB NOT NULL,
311
+ dimensions INTEGER NOT NULL,
312
+ updated_at INTEGER NOT NULL
313
+ );
314
+
315
+ CREATE VIRTUAL TABLE IF NOT EXISTS tasks_fts USING fts5(
316
+ summary,
317
+ topic,
318
+ content='tasks',
319
+ content_rowid='rowid',
320
+ tokenize='trigram'
321
+ );
322
+ `);
323
+
324
+ try {
325
+ this.db.exec(`
326
+ CREATE TRIGGER IF NOT EXISTS tasks_fts_ai AFTER INSERT ON tasks BEGIN
327
+ INSERT INTO tasks_fts(rowid, summary, topic)
328
+ VALUES (new.rowid, new.summary, COALESCE(new.topic, ''));
329
+ END;
330
+ CREATE TRIGGER IF NOT EXISTS tasks_fts_ad AFTER DELETE ON tasks BEGIN
331
+ INSERT INTO tasks_fts(tasks_fts, rowid, summary, topic)
332
+ VALUES ('delete', old.rowid, old.summary, COALESCE(old.topic, ''));
333
+ END;
334
+ CREATE TRIGGER IF NOT EXISTS tasks_fts_au AFTER UPDATE ON tasks BEGIN
335
+ INSERT INTO tasks_fts(tasks_fts, rowid, summary, topic)
336
+ VALUES ('delete', old.rowid, old.summary, COALESCE(old.topic, ''));
337
+ INSERT INTO tasks_fts(rowid, summary, topic)
338
+ VALUES (new.rowid, new.summary, COALESCE(new.topic, ''));
339
+ END;
340
+ `);
341
+ } catch {
342
+ // triggers may already exist
343
+ }
344
+
345
+ try {
346
+ const count = (this.db.prepare("SELECT COUNT(*) as c FROM tasks_fts").get() as { c: number }).c;
347
+ const taskCount = (this.db.prepare("SELECT COUNT(*) as c FROM tasks").get() as { c: number }).c;
348
+ if (count === 0 && taskCount > 0) {
349
+ this.db.exec("INSERT INTO tasks_fts(rowid, summary, topic) SELECT rowid, summary, COALESCE(topic, '') FROM tasks");
350
+ this.log.info(`Migrated: backfilled tasks_fts for ${taskCount} tasks`);
351
+ }
352
+ } catch { /* best-effort */ }
353
+ }
354
+
293
355
  private migrateFtsToTrigram(): void {
294
356
  // Check if chunks_fts still uses the old tokenizer (porter unicode61)
295
357
  try {
@@ -507,6 +569,14 @@ export class SqliteStore {
507
569
  }
508
570
  }
509
571
 
572
+ private migrateTaskTopicColumn(): void {
573
+ const cols = this.db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
574
+ if (!cols.some((c) => c.name === "topic")) {
575
+ this.db.exec("ALTER TABLE tasks ADD COLUMN topic TEXT DEFAULT NULL");
576
+ this.log.info("Migrated: added topic column to tasks");
577
+ }
578
+ }
579
+
510
580
  private migrateTaskSkillMeta(): void {
511
581
  const cols = this.db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
512
582
  if (!cols.some((c) => c.name === "skill_status")) {
@@ -992,6 +1062,7 @@ export class SqliteStore {
992
1062
  id TEXT PRIMARY KEY,
993
1063
  source_chunk_id TEXT NOT NULL,
994
1064
  source_user_id TEXT NOT NULL,
1065
+ source_agent TEXT NOT NULL DEFAULT '',
995
1066
  role TEXT NOT NULL,
996
1067
  content TEXT NOT NULL,
997
1068
  summary TEXT NOT NULL DEFAULT '',
@@ -1207,7 +1278,7 @@ export class SqliteStore {
1207
1278
 
1208
1279
  // ─── Pattern Search (LIKE-based, for CJK text where FTS tokenization is weak) ───
1209
1280
 
1210
- patternSearch(patterns: string[], opts: { role?: string; limit?: number } = {}): Array<{ chunkId: string; content: string; role: string; createdAt: number }> {
1281
+ patternSearch(patterns: string[], opts: { role?: string; limit?: number; ownerFilter?: string[] } = {}): Array<{ chunkId: string; content: string; role: string; createdAt: number }> {
1211
1282
  if (patterns.length === 0) return [];
1212
1283
  const limit = opts.limit ?? 10;
1213
1284
 
@@ -1216,13 +1287,21 @@ export class SqliteStore {
1216
1287
  const roleClause = opts.role ? " AND c.role = ?" : "";
1217
1288
  const params: (string | number)[] = patterns.map(p => `%${p}%`);
1218
1289
  if (opts.role) params.push(opts.role);
1290
+
1291
+ let ownerClause = "";
1292
+ if (opts.ownerFilter && opts.ownerFilter.length > 0) {
1293
+ const placeholders = opts.ownerFilter.map(() => "?").join(",");
1294
+ ownerClause = ` AND c.owner IN (${placeholders})`;
1295
+ params.push(...opts.ownerFilter);
1296
+ }
1297
+
1219
1298
  params.push(limit);
1220
1299
 
1221
1300
  try {
1222
1301
  const rows = this.db.prepare(`
1223
1302
  SELECT c.id as chunk_id, c.content, c.role, c.created_at
1224
1303
  FROM chunks c
1225
- WHERE (${whereClause})${roleClause} AND c.dedup_status = 'active'
1304
+ WHERE (${whereClause})${roleClause}${ownerClause} AND c.dedup_status = 'active'
1226
1305
  ORDER BY c.created_at DESC
1227
1306
  LIMIT ?
1228
1307
  `).all(...params) as Array<{ chunk_id: string; content: string; role: string; created_at: number }>;
@@ -1425,8 +1504,15 @@ export class SqliteStore {
1425
1504
 
1426
1505
  deleteAll(): number {
1427
1506
  this.db.exec("PRAGMA foreign_keys = OFF");
1507
+ try {
1508
+ this.db.exec("DROP TRIGGER IF EXISTS tasks_fts_ai");
1509
+ this.db.exec("DROP TRIGGER IF EXISTS tasks_fts_ad");
1510
+ this.db.exec("DROP TRIGGER IF EXISTS tasks_fts_au");
1511
+ this.db.exec("DELETE FROM tasks_fts");
1512
+ } catch (_) {}
1428
1513
  const tables = [
1429
1514
  "task_skills",
1515
+ "task_embeddings",
1430
1516
  "skill_embeddings",
1431
1517
  "skill_versions",
1432
1518
  "skills",
@@ -1449,6 +1535,7 @@ export class SqliteStore {
1449
1535
  }
1450
1536
  }
1451
1537
  this.db.exec("PRAGMA foreign_keys = ON");
1538
+ this.migrateTaskEmbeddingsAndFts();
1452
1539
  const remaining = this.countChunks();
1453
1540
  return remaining === 0 ? 1 : 0;
1454
1541
  }
@@ -1469,6 +1556,21 @@ export class SqliteStore {
1469
1556
  return result.changes > 0;
1470
1557
  }
1471
1558
 
1559
+ disableSkill(skillId: string): boolean {
1560
+ const skill = this.getSkill(skillId);
1561
+ if (!skill || skill.status === "archived") return false;
1562
+ this.db.prepare("DELETE FROM skill_embeddings WHERE skill_id = ?").run(skillId);
1563
+ this.updateSkill(skillId, { status: "archived", installed: 0 });
1564
+ return true;
1565
+ }
1566
+
1567
+ enableSkill(skillId: string): boolean {
1568
+ const skill = this.getSkill(skillId);
1569
+ if (!skill || skill.status !== "archived") return false;
1570
+ this.updateSkill(skillId, { status: "active" });
1571
+ return true;
1572
+ }
1573
+
1472
1574
  // ─── Task CRUD ───
1473
1575
 
1474
1576
  insertTask(task: Task): void {
@@ -1550,10 +1652,11 @@ export class SqliteStore {
1550
1652
  return rows.map(rowToChunk);
1551
1653
  }
1552
1654
 
1553
- listTasks(opts: { status?: string; limit?: number; offset?: number; owner?: string } = {}): { tasks: Task[]; total: number } {
1655
+ listTasks(opts: { status?: string; limit?: number; offset?: number; owner?: string; session?: string } = {}): { tasks: Task[]; total: number } {
1554
1656
  const conditions: string[] = [];
1555
1657
  const params: unknown[] = [];
1556
1658
  if (opts.status) { conditions.push("status = ?"); params.push(opts.status); }
1659
+ if (opts.session) { conditions.push("session_key = ?"); params.push(opts.session); }
1557
1660
  if (opts.owner) {
1558
1661
  conditions.push("(owner = ? OR (owner = 'public' AND id IN (SELECT task_id FROM local_shared_tasks WHERE original_owner = ?)))");
1559
1662
  params.push(opts.owner, opts.owner);
@@ -1675,9 +1778,24 @@ export class SqliteStore {
1675
1778
  this.db.prepare(`UPDATE skills SET ${sets.join(", ")} WHERE id = ?`).run(...params);
1676
1779
  }
1677
1780
 
1678
- listSkills(opts: { status?: string } = {}): Skill[] {
1679
- const cond = opts.status ? "WHERE status = ?" : "";
1680
- const params = opts.status ? [opts.status] : [];
1781
+ listSkills(opts: { status?: string; session?: string; owner?: string } = {}): Skill[] {
1782
+ const conditions: string[] = [];
1783
+ const params: unknown[] = [];
1784
+ if (opts.status) { conditions.push("status = ?"); params.push(opts.status); }
1785
+ if (opts.owner) {
1786
+ conditions.push("(owner = ? OR owner = 'public')");
1787
+ params.push(opts.owner);
1788
+ }
1789
+ if (opts.session) {
1790
+ conditions.push(`EXISTS (
1791
+ SELECT 1
1792
+ FROM task_skills ts
1793
+ JOIN tasks t ON t.id = ts.task_id
1794
+ WHERE ts.skill_id = skills.id AND t.session_key = ?
1795
+ )`);
1796
+ params.push(opts.session);
1797
+ }
1798
+ const cond = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1681
1799
  const rows = this.db.prepare(`SELECT * FROM skills ${cond} ORDER BY updated_at DESC`).all(...params) as SkillRow[];
1682
1800
  return rows.map(rowToSkill);
1683
1801
  }
@@ -1767,6 +1885,61 @@ export class SqliteStore {
1767
1885
  }
1768
1886
  }
1769
1887
 
1888
+ // ─── Task Embeddings & Search ───
1889
+
1890
+ upsertTaskEmbedding(taskId: string, vector: number[]): void {
1891
+ const buf = Buffer.from(new Float32Array(vector).buffer);
1892
+ this.db.prepare(`
1893
+ INSERT OR REPLACE INTO task_embeddings (task_id, vector, dimensions, updated_at)
1894
+ VALUES (?, ?, ?, ?)
1895
+ `).run(taskId, buf, vector.length, Date.now());
1896
+ }
1897
+
1898
+ getTaskEmbeddings(owner?: string): Array<{ taskId: string; vector: number[] }> {
1899
+ let sql = `SELECT te.task_id, te.vector, te.dimensions
1900
+ FROM task_embeddings te
1901
+ JOIN tasks t ON t.id = te.task_id`;
1902
+ const params: any[] = [];
1903
+ if (owner) {
1904
+ sql += ` WHERE (t.owner = ? OR t.owner = 'public')`;
1905
+ params.push(owner);
1906
+ }
1907
+ const rows = this.db.prepare(sql).all(...params) as Array<{ task_id: string; vector: Buffer; dimensions: number }>;
1908
+ return rows.map((r) => ({
1909
+ taskId: r.task_id,
1910
+ vector: Array.from(new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions)),
1911
+ }));
1912
+ }
1913
+
1914
+ taskFtsSearch(query: string, limit: number, owner?: string): Array<{ taskId: string; score: number }> {
1915
+ const sanitized = sanitizeFtsQuery(query);
1916
+ if (!sanitized) return [];
1917
+ try {
1918
+ let sql = `
1919
+ SELECT t.id as task_id, rank
1920
+ FROM tasks_fts f
1921
+ JOIN tasks t ON t.rowid = f.rowid
1922
+ WHERE tasks_fts MATCH ?`;
1923
+ const params: any[] = [sanitized];
1924
+ if (owner) {
1925
+ sql += ` AND (t.owner = ? OR t.owner = 'public')`;
1926
+ params.push(owner);
1927
+ }
1928
+ sql += ` ORDER BY rank LIMIT ?`;
1929
+ params.push(limit);
1930
+ const rows = this.db.prepare(sql).all(...params) as Array<{ task_id: string; rank: number }>;
1931
+ if (rows.length === 0) return [];
1932
+ const maxAbsRank = Math.max(...rows.map((r) => Math.abs(r.rank)));
1933
+ return rows.map((r) => ({
1934
+ taskId: r.task_id,
1935
+ score: maxAbsRank > 0 ? Math.abs(r.rank) / maxAbsRank : 0,
1936
+ }));
1937
+ } catch {
1938
+ this.log.warn(`Task FTS query failed for: "${sanitized}", returning empty`);
1939
+ return [];
1940
+ }
1941
+ }
1942
+
1770
1943
  listPublicSkills(): Skill[] {
1771
1944
  const rows = this.db.prepare("SELECT * FROM skills WHERE visibility = 'public' AND status = 'active' ORDER BY updated_at DESC").all() as SkillRow[];
1772
1945
  return rows.map(rowToSkill);
@@ -2430,9 +2603,10 @@ export class SqliteStore {
2430
2603
 
2431
2604
  upsertHubMemory(memory: HubMemoryRecord): void {
2432
2605
  this.db.prepare(`
2433
- INSERT INTO hub_memories (id, source_chunk_id, source_user_id, role, content, summary, kind, group_id, visibility, created_at, updated_at)
2434
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2606
+ INSERT INTO hub_memories (id, source_chunk_id, source_user_id, source_agent, role, content, summary, kind, group_id, visibility, created_at, updated_at)
2607
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2435
2608
  ON CONFLICT(source_user_id, source_chunk_id) DO UPDATE SET
2609
+ source_agent = excluded.source_agent,
2436
2610
  role = excluded.role,
2437
2611
  content = excluded.content,
2438
2612
  summary = excluded.summary,
@@ -2441,7 +2615,7 @@ export class SqliteStore {
2441
2615
  visibility = excluded.visibility,
2442
2616
  created_at = excluded.created_at,
2443
2617
  updated_at = excluded.updated_at
2444
- `).run(memory.id, memory.sourceChunkId, memory.sourceUserId, memory.role, memory.content, memory.summary, memory.kind, memory.groupId, memory.visibility, memory.createdAt, memory.updatedAt);
2618
+ `).run(memory.id, memory.sourceChunkId, memory.sourceUserId, memory.sourceAgent, memory.role, memory.content, memory.summary, memory.kind, memory.groupId, memory.visibility, memory.createdAt, memory.updatedAt);
2445
2619
  }
2446
2620
 
2447
2621
  getHubMemoryBySource(sourceUserId: string, sourceChunkId: string): HubMemoryRecord | null {
@@ -2550,6 +2724,11 @@ export class SqliteStore {
2550
2724
  this.db.prepare("UPDATE local_shared_tasks SET hub_task_id = '', hub_instance_id = '', visibility = 'public', group_id = NULL, synced_chunks = 0 WHERE task_id = ?").run(taskId);
2551
2725
  }
2552
2726
 
2727
+ /** Client UI: remove team_shared_chunks rows for all chunks linked to this task (list badge chunk fallback). */
2728
+ clearTeamSharedChunksForTask(taskId: string): void {
2729
+ this.db.prepare("DELETE FROM team_shared_chunks WHERE chunk_id IN (SELECT id FROM chunks WHERE task_id = ?)").run(taskId);
2730
+ }
2731
+
2553
2732
  clearAllTeamSharingState(): void {
2554
2733
  this.clearTeamSharedChunks();
2555
2734
  this.clearTeamSharedSkills();
@@ -2607,7 +2786,7 @@ export class SqliteStore {
2607
2786
  if (!sanitized) return [];
2608
2787
  const rows = this.db.prepare(`
2609
2788
  SELECT hm.id, hm.content, hm.summary, hm.role, hm.created_at, hm.visibility, '' as group_name, hu.username as owner_name,
2610
- bm25(hub_memories_fts) as rank
2789
+ COALESCE(hm.source_agent, '') as source_agent, bm25(hub_memories_fts) as rank
2611
2790
  FROM hub_memories_fts f
2612
2791
  JOIN hub_memories hm ON hm.rowid = f.rowid
2613
2792
  LEFT JOIN hub_users hu ON hu.id = hm.source_user_id
@@ -2623,7 +2802,7 @@ export class SqliteStore {
2623
2802
  getVisibleHubSearchHitByMemoryId(memoryId: string, userId: string): HubMemorySearchRow | null {
2624
2803
  const row = this.db.prepare(`
2625
2804
  SELECT hm.id, hm.content, hm.summary, hm.role, hm.created_at, hm.visibility, '' as group_name, hu.username as owner_name,
2626
- 0 as rank
2805
+ COALESCE(hm.source_agent, '') as source_agent, 0 as rank
2627
2806
  FROM hub_memories hm
2628
2807
  LEFT JOIN hub_users hu ON hu.id = hm.source_user_id
2629
2808
  WHERE hm.id = ?
@@ -3117,6 +3296,7 @@ export interface HubMemoryRecord {
3117
3296
  id: string;
3118
3297
  sourceChunkId: string;
3119
3298
  sourceUserId: string;
3299
+ sourceAgent: string;
3120
3300
  role: string;
3121
3301
  content: string;
3122
3302
  summary: string;
@@ -3131,6 +3311,7 @@ interface HubMemoryRow {
3131
3311
  id: string;
3132
3312
  source_chunk_id: string;
3133
3313
  source_user_id: string;
3314
+ source_agent: string;
3134
3315
  role: string;
3135
3316
  content: string;
3136
3317
  summary: string;
@@ -3146,6 +3327,7 @@ function rowToHubMemory(row: HubMemoryRow): HubMemoryRecord {
3146
3327
  id: row.id,
3147
3328
  sourceChunkId: row.source_chunk_id,
3148
3329
  sourceUserId: row.source_user_id,
3330
+ sourceAgent: row.source_agent || "",
3149
3331
  role: row.role,
3150
3332
  content: row.content,
3151
3333
  summary: row.summary,
@@ -3166,6 +3348,7 @@ interface HubMemorySearchRow {
3166
3348
  visibility: string;
3167
3349
  group_name: string | null;
3168
3350
  owner_name: string | null;
3351
+ source_agent: string;
3169
3352
  rank: number;
3170
3353
  }
3171
3354
 
package/src/types.ts CHANGED
@@ -322,6 +322,8 @@ export interface MemosLocalConfig {
322
322
  skillEvolution?: SkillEvolutionConfig;
323
323
  telemetry?: TelemetryConfig;
324
324
  sharing?: SharingConfig;
325
+ /** Hours of inactivity after which an active task is automatically finalized. 0 = disabled. Default 4. */
326
+ taskAutoFinalizeHours?: number;
325
327
  }
326
328
 
327
329
  // ─── Defaults ───
@@ -357,6 +359,7 @@ export const DEFAULTS = {
357
359
  skillAutoRecallLimit: 2,
358
360
  skillPreferUpgrade: true,
359
361
  skillRedactSensitive: true,
362
+ taskAutoFinalizeHours: 4,
360
363
  } as const;
361
364
 
362
365
  // ─── Plugin Hooks (OpenClaw integration) ───