@memtensor/memos-local-openclaw-plugin 0.3.18 → 0.3.20

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.
Files changed (69) hide show
  1. package/README.md +21 -11
  2. package/dist/capture/index.d.ts +1 -1
  3. package/dist/capture/index.d.ts.map +1 -1
  4. package/dist/capture/index.js +7 -2
  5. package/dist/capture/index.js.map +1 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2 -2
  9. package/dist/index.js.map +1 -1
  10. package/dist/ingest/dedup.d.ts +2 -2
  11. package/dist/ingest/dedup.d.ts.map +1 -1
  12. package/dist/ingest/dedup.js +4 -4
  13. package/dist/ingest/dedup.js.map +1 -1
  14. package/dist/ingest/task-processor.d.ts +1 -1
  15. package/dist/ingest/task-processor.d.ts.map +1 -1
  16. package/dist/ingest/task-processor.js +14 -13
  17. package/dist/ingest/task-processor.js.map +1 -1
  18. package/dist/ingest/worker.d.ts.map +1 -1
  19. package/dist/ingest/worker.js +7 -3
  20. package/dist/ingest/worker.js.map +1 -1
  21. package/dist/recall/engine.d.ts +5 -1
  22. package/dist/recall/engine.d.ts.map +1 -1
  23. package/dist/recall/engine.js +77 -2
  24. package/dist/recall/engine.js.map +1 -1
  25. package/dist/skill/evolver.d.ts +2 -1
  26. package/dist/skill/evolver.d.ts.map +1 -1
  27. package/dist/skill/evolver.js +2 -2
  28. package/dist/skill/evolver.js.map +1 -1
  29. package/dist/skill/generator.d.ts +3 -1
  30. package/dist/skill/generator.d.ts.map +1 -1
  31. package/dist/skill/generator.js +15 -1
  32. package/dist/skill/generator.js.map +1 -1
  33. package/dist/storage/sqlite.d.ts +24 -8
  34. package/dist/storage/sqlite.d.ts.map +1 -1
  35. package/dist/storage/sqlite.js +233 -28
  36. package/dist/storage/sqlite.js.map +1 -1
  37. package/dist/storage/vector.d.ts +1 -1
  38. package/dist/storage/vector.d.ts.map +1 -1
  39. package/dist/storage/vector.js +3 -3
  40. package/dist/storage/vector.js.map +1 -1
  41. package/dist/types.d.ts +16 -0
  42. package/dist/types.d.ts.map +1 -1
  43. package/dist/types.js.map +1 -1
  44. package/dist/viewer/html.d.ts +1 -1
  45. package/dist/viewer/html.d.ts.map +1 -1
  46. package/dist/viewer/html.js +107 -1
  47. package/dist/viewer/html.js.map +1 -1
  48. package/dist/viewer/server.d.ts +1 -0
  49. package/dist/viewer/server.d.ts.map +1 -1
  50. package/dist/viewer/server.js +52 -3
  51. package/dist/viewer/server.js.map +1 -1
  52. package/index.ts +187 -7
  53. package/package.json +1 -1
  54. package/skill/browserwing-admin/SKILL.md +521 -0
  55. package/skill/browserwing-executor/SKILL.md +510 -0
  56. package/skill/memos-memory-guide/SKILL.md +62 -36
  57. package/src/capture/index.ts +7 -1
  58. package/src/index.ts +3 -2
  59. package/src/ingest/dedup.ts +4 -2
  60. package/src/ingest/task-processor.ts +14 -13
  61. package/src/ingest/worker.ts +7 -3
  62. package/src/recall/engine.ts +94 -4
  63. package/src/skill/evolver.ts +3 -1
  64. package/src/skill/generator.ts +15 -0
  65. package/src/storage/sqlite.ts +262 -34
  66. package/src/storage/vector.ts +3 -2
  67. package/src/types.ts +18 -0
  68. package/src/viewer/html.ts +107 -1
  69. package/src/viewer/server.ts +48 -3
@@ -7,6 +7,10 @@ const SELF_TOOLS = new Set([
7
7
  "memory_timeline",
8
8
  "memory_get",
9
9
  "memory_viewer",
10
+ "memory_write_public",
11
+ "skill_search",
12
+ "skill_publish",
13
+ "skill_unpublish",
10
14
  ]);
11
15
 
12
16
  // OpenClaw inbound metadata sentinels — these are AI-facing prefixes,
@@ -37,6 +41,7 @@ export function captureMessages(
37
41
  turnId: string,
38
42
  _evidenceTag: string,
39
43
  log: Logger,
44
+ owner?: string,
40
45
  ): ConversationMessage[] {
41
46
  const now = Date.now();
42
47
  const result: ConversationMessage[] = [];
@@ -64,10 +69,11 @@ export function captureMessages(
64
69
  turnId,
65
70
  sessionKey,
66
71
  toolName: role === "tool" ? msg.toolName : undefined,
72
+ owner: owner ?? "agent:main",
67
73
  });
68
74
  }
69
75
 
70
- log.debug(`Captured ${result.length}/${messages.length} messages for session=${sessionKey} turn=${turnId}`);
76
+ log.debug(`Captured ${result.length}/${messages.length} messages for session=${sessionKey} turn=${turnId} owner=${owner ?? "agent:main"}`);
71
77
  return result;
72
78
  }
73
79
 
package/src/index.ts CHANGED
@@ -11,7 +11,7 @@ import type { MemosLocalConfig, ToolDefinition, Logger } from "./types";
11
11
  export interface MemosLocalPlugin {
12
12
  id: string;
13
13
  tools: ToolDefinition[];
14
- onConversationTurn: (messages: Array<{ role: string; content: string }>, sessionKey?: string) => void;
14
+ onConversationTurn: (messages: Array<{ role: string; content: string }>, sessionKey?: string, owner?: string) => void;
15
15
  /** Wait for all pending ingest operations to complete. */
16
16
  flush: () => Promise<void>;
17
17
  shutdown: () => void;
@@ -75,12 +75,13 @@ export function initPlugin(opts: PluginInitOptions = {}): MemosLocalPlugin {
75
75
  onConversationTurn(
76
76
  messages: Array<{ role: string; content: string }>,
77
77
  sessionKey?: string,
78
+ owner?: string,
78
79
  ): void {
79
80
  const session = sessionKey ?? "default";
80
81
  const turnId = uuid();
81
82
  const tag = ctx.config.capture?.evidenceWrapperTag ?? "STORED_MEMORY";
82
83
 
83
- const captured = captureMessages(messages, session, turnId, tag, ctx.log);
84
+ const captured = captureMessages(messages, session, turnId, tag, ctx.log, owner);
84
85
  if (captured.length > 0) {
85
86
  worker.enqueue(captured);
86
87
  }
@@ -14,8 +14,9 @@ export function findDuplicate(
14
14
  newVec: number[],
15
15
  threshold: number,
16
16
  log: Logger,
17
+ ownerFilter?: string[],
17
18
  ): string | null {
18
- const all = store.getAllEmbeddings();
19
+ const all = store.getAllEmbeddings(ownerFilter);
19
20
 
20
21
  let bestId: string | null = null;
21
22
  let bestScore = 0;
@@ -46,8 +47,9 @@ export function findTopSimilar(
46
47
  threshold: number,
47
48
  topN: number,
48
49
  log: Logger,
50
+ ownerFilter?: string[],
49
51
  ): Array<{ chunkId: string; score: number }> {
50
- const all = store.getAllEmbeddings();
52
+ const all = store.getAllEmbeddings(ownerFilter);
51
53
  const scored: Array<{ chunkId: string; score: number }> = [];
52
54
 
53
55
  for (const { chunkId, vector } of all) {
@@ -47,15 +47,15 @@ export class TaskProcessor {
47
47
  * Called after new chunks are ingested.
48
48
  * Determines if a new task boundary was crossed and handles transition.
49
49
  */
50
- async onChunksIngested(sessionKey: string, latestTimestamp: number): Promise<void> {
51
- this.ctx.log.debug(`TaskProcessor.onChunksIngested called session=${sessionKey} ts=${latestTimestamp} processing=${this.processing}`);
50
+ async onChunksIngested(sessionKey: string, latestTimestamp: number, owner?: string): Promise<void> {
51
+ this.ctx.log.debug(`TaskProcessor.onChunksIngested called session=${sessionKey} ts=${latestTimestamp} owner=${owner ?? "agent:main"} processing=${this.processing}`);
52
52
  if (this.processing) {
53
53
  this.ctx.log.debug("TaskProcessor.onChunksIngested skipped — already processing");
54
54
  return;
55
55
  }
56
56
  this.processing = true;
57
57
  try {
58
- await this.detectAndProcess(sessionKey, latestTimestamp);
58
+ await this.detectAndProcess(sessionKey, latestTimestamp, owner ?? "agent:main");
59
59
  } catch (err) {
60
60
  this.ctx.log.error(`TaskProcessor error: ${err}`);
61
61
  } finally {
@@ -63,23 +63,23 @@ export class TaskProcessor {
63
63
  }
64
64
  }
65
65
 
66
- private async detectAndProcess(sessionKey: string, latestTimestamp: number): Promise<void> {
67
- this.ctx.log.debug(`TaskProcessor.detectAndProcess session=${sessionKey}`);
66
+ private async detectAndProcess(sessionKey: string, latestTimestamp: number, owner: string): Promise<void> {
67
+ this.ctx.log.debug(`TaskProcessor.detectAndProcess session=${sessionKey} owner=${owner}`);
68
68
 
69
- // Finalize any active tasks from OTHER sessions (session change = task boundary)
70
- const allActive = this.store.getAllActiveTasks();
69
+ // Finalize any active tasks from OTHER sessions for the SAME owner (session change = task boundary)
70
+ const allActive = this.store.getAllActiveTasks(owner);
71
71
  for (const t of allActive) {
72
72
  if (t.sessionKey !== sessionKey) {
73
- this.ctx.log.info(`Session changed: finalizing task=${t.id} from session=${t.sessionKey}`);
73
+ this.ctx.log.info(`Session changed: finalizing task=${t.id} from session=${t.sessionKey} (owner=${owner})`);
74
74
  await this.finalizeTask(t);
75
75
  }
76
76
  }
77
77
 
78
- const activeTask = this.store.getActiveTask(sessionKey);
79
- this.ctx.log.debug(`TaskProcessor.detectAndProcess activeTask=${activeTask?.id ?? "none"}`);
78
+ const activeTask = this.store.getActiveTask(sessionKey, owner);
79
+ this.ctx.log.debug(`TaskProcessor.detectAndProcess activeTask=${activeTask?.id ?? "none"} owner=${owner}`);
80
80
 
81
81
  if (!activeTask) {
82
- await this.createNewTask(sessionKey, latestTimestamp);
82
+ await this.createNewTask(sessionKey, latestTimestamp, owner);
83
83
  return;
84
84
  }
85
85
 
@@ -87,7 +87,7 @@ export class TaskProcessor {
87
87
 
88
88
  if (isNewTask) {
89
89
  await this.finalizeTask(activeTask);
90
- await this.createNewTask(sessionKey, latestTimestamp);
90
+ await this.createNewTask(sessionKey, latestTimestamp, owner);
91
91
  } else {
92
92
  this.assignUnassignedChunks(sessionKey, activeTask.id);
93
93
  this.store.updateTask(activeTask.id, { endedAt: undefined });
@@ -151,7 +151,7 @@ export class TaskProcessor {
151
151
  .join("\n");
152
152
  }
153
153
 
154
- private async createNewTask(sessionKey: string, timestamp: number): Promise<void> {
154
+ private async createNewTask(sessionKey: string, timestamp: number, owner: string = "agent:main"): Promise<void> {
155
155
  const taskId = uuid();
156
156
  const task: Task = {
157
157
  id: taskId,
@@ -159,6 +159,7 @@ export class TaskProcessor {
159
159
  title: "",
160
160
  summary: "",
161
161
  status: "active",
162
+ owner,
162
163
  startedAt: timestamp,
163
164
  endedAt: null,
164
165
  updatedAt: timestamp,
@@ -48,6 +48,7 @@ export class IngestWorker {
48
48
  const t0 = performance.now();
49
49
 
50
50
  let lastSessionKey: string | undefined;
51
+ let lastOwner: string | undefined;
51
52
  let lastTimestamp = 0;
52
53
  let stored = 0;
53
54
  let skipped = 0;
@@ -64,6 +65,7 @@ export class IngestWorker {
64
65
  try {
65
66
  const result = await this.ingestMessage(msg);
66
67
  lastSessionKey = msg.sessionKey;
68
+ lastOwner = msg.owner ?? "agent:main";
67
69
  lastTimestamp = Math.max(lastTimestamp, msg.timestamp);
68
70
  if (result === "skipped") {
69
71
  skipped++;
@@ -101,9 +103,9 @@ export class IngestWorker {
101
103
  }
102
104
 
103
105
  if (lastSessionKey) {
104
- this.ctx.log.debug(`Calling TaskProcessor.onChunksIngested session=${lastSessionKey} ts=${lastTimestamp}`);
106
+ this.ctx.log.debug(`Calling TaskProcessor.onChunksIngested session=${lastSessionKey} ts=${lastTimestamp} owner=${lastOwner}`);
105
107
  this.taskProcessor
106
- .onChunksIngested(lastSessionKey, lastTimestamp)
108
+ .onChunksIngested(lastSessionKey, lastTimestamp, lastOwner)
107
109
  .catch((err) => this.ctx.log.error(`TaskProcessor post-ingest error: ${err}`));
108
110
  }
109
111
 
@@ -147,7 +149,8 @@ export class IngestWorker {
147
149
  // Smart dedup: find Top-5 similar chunks, then ask LLM to judge
148
150
  if (embedding) {
149
151
  const similarThreshold = this.ctx.config.dedup?.similarityThreshold ?? 0.75;
150
- const topSimilar = findTopSimilar(this.store, embedding, similarThreshold, 5, this.ctx.log);
152
+ const dedupOwnerFilter = msg.owner ? [msg.owner, "public"] : undefined;
153
+ const topSimilar = findTopSimilar(this.store, embedding, similarThreshold, 5, this.ctx.log, dedupOwnerFilter);
151
154
 
152
155
  if (topSimilar.length > 0) {
153
156
  const candidates = topSimilar.map((s, i) => {
@@ -214,6 +217,7 @@ export class IngestWorker {
214
217
  embedding: null,
215
218
  taskId: null,
216
219
  skillId: null,
220
+ owner: msg.owner ?? "agent:main",
217
221
  dedupStatus,
218
222
  dedupTarget,
219
223
  dedupReason,
@@ -1,16 +1,20 @@
1
1
  import type { SqliteStore } from "../storage/sqlite";
2
2
  import type { Embedder } from "../embedding";
3
- import type { PluginContext, SearchHit, SearchResult } from "../types";
4
- import { vectorSearch } from "../storage/vector";
3
+ import type { PluginContext, SearchHit, SearchResult, SkillSearchHit, Skill } from "../types";
4
+ import { vectorSearch, cosineSimilarity } from "../storage/vector";
5
5
  import { rrfFuse } from "./rrf";
6
6
  import { mmrRerank } from "./mmr";
7
7
  import { applyRecencyDecay } from "./recency";
8
+ import { Summarizer } from "../ingest/providers";
9
+
10
+ export type SkillSearchScope = "mix" | "self" | "public";
8
11
 
9
12
  export interface RecallOptions {
10
13
  query?: string;
11
14
  maxResults?: number;
12
15
  minScore?: number;
13
16
  role?: string;
17
+ ownerFilter?: string[];
14
18
  }
15
19
 
16
20
  const MAX_RECENT_QUERIES = 20;
@@ -36,10 +40,11 @@ export class RecallEngine {
36
40
 
37
41
  const repeatNote = this.checkRepeat(query, maxResults, minScore);
38
42
  const candidatePool = maxResults * 5;
43
+ const ownerFilter = opts.ownerFilter;
39
44
 
40
45
  // Step 1: Gather candidates from both FTS and vector search
41
46
  const ftsCandidates = query
42
- ? this.store.ftsSearch(query, candidatePool)
47
+ ? this.store.ftsSearch(query, candidatePool, ownerFilter)
43
48
  : [];
44
49
 
45
50
  let vecCandidates: Array<{ chunkId: string; score: number }> = [];
@@ -49,7 +54,7 @@ export class RecallEngine {
49
54
  const maxChunks = recallCfg.vectorSearchMaxChunks && recallCfg.vectorSearchMaxChunks > 0
50
55
  ? recallCfg.vectorSearchMaxChunks
51
56
  : undefined;
52
- vecCandidates = vectorSearch(this.store, queryVec, candidatePool, maxChunks);
57
+ vecCandidates = vectorSearch(this.store, queryVec, candidatePool, maxChunks, ownerFilter);
53
58
  } catch (err) {
54
59
  this.ctx.log.warn(`Vector search failed, using FTS only: ${err}`);
55
60
  }
@@ -181,6 +186,91 @@ export class RecallEngine {
181
186
  this.recentQueries.shift();
182
187
  }
183
188
  }
189
+
190
+ async searchSkills(query: string, scope: SkillSearchScope, currentOwner: string): Promise<SkillSearchHit[]> {
191
+ const RRF_K = 60;
192
+ const TOP_CANDIDATES = 20;
193
+
194
+ // FTS on name + description
195
+ const ftsCandidates = this.store.skillFtsSearch(query, TOP_CANDIDATES, scope, currentOwner);
196
+
197
+ // Vector search on description embedding
198
+ let vecCandidates: Array<{ skillId: string; score: number }> = [];
199
+ try {
200
+ const queryVec = await this.embedder.embedQuery(query);
201
+ const allEmb = this.store.getSkillEmbeddings(scope, currentOwner);
202
+ vecCandidates = allEmb.map((row) => ({
203
+ skillId: row.skillId,
204
+ score: cosineSimilarity(queryVec, row.vector),
205
+ }));
206
+ vecCandidates.sort((a, b) => b.score - a.score);
207
+ vecCandidates = vecCandidates.slice(0, TOP_CANDIDATES);
208
+ } catch (err) {
209
+ this.ctx.log.warn(`Skill vector search failed, using FTS only: ${err}`);
210
+ }
211
+
212
+ // RRF fusion
213
+ const ftsRanked = ftsCandidates.map((c) => ({ id: c.skillId, score: c.score }));
214
+ const vecRanked = vecCandidates.map((c) => ({ id: c.skillId, score: c.score }));
215
+ const rrfScores = rrfFuse([ftsRanked, vecRanked], RRF_K);
216
+
217
+ if (rrfScores.size === 0) return [];
218
+
219
+ const sorted = [...rrfScores.entries()]
220
+ .map(([id, score]) => ({ id, score }))
221
+ .sort((a, b) => b.score - a.score)
222
+ .slice(0, TOP_CANDIDATES);
223
+
224
+ // Load skill details for LLM judgment
225
+ const candidateSkills: Array<{ skill: Skill; rrfScore: number }> = [];
226
+ for (const item of sorted) {
227
+ const skill = this.store.getSkill(item.id);
228
+ if (skill) candidateSkills.push({ skill, rrfScore: item.score });
229
+ }
230
+
231
+ if (candidateSkills.length === 0) return [];
232
+
233
+ // LLM relevance judgment
234
+ const summarizer = new Summarizer(this.ctx.config.summarizer, this.ctx.log);
235
+ const relevantIndices = await this.judgeSkillRelevance(summarizer, query, candidateSkills);
236
+
237
+ return relevantIndices.map((idx) => {
238
+ const { skill, rrfScore } = candidateSkills[idx];
239
+ return {
240
+ skillId: skill.id,
241
+ name: skill.name,
242
+ description: skill.description,
243
+ owner: skill.owner,
244
+ visibility: skill.visibility,
245
+ score: rrfScore,
246
+ reason: "relevant",
247
+ };
248
+ });
249
+ }
250
+
251
+ private async judgeSkillRelevance(
252
+ summarizer: Summarizer,
253
+ query: string,
254
+ candidates: Array<{ skill: Skill; rrfScore: number }>,
255
+ ): Promise<number[]> {
256
+ const candidateList = candidates.map((c, i) => ({
257
+ index: i,
258
+ summary: `[${c.skill.name}] ${c.skill.description}`,
259
+ role: "skill" as const,
260
+ }));
261
+
262
+ try {
263
+ const result = await summarizer.filterRelevant(query, candidateList);
264
+ if (result && result.relevant.length > 0) {
265
+ return result.relevant.map((r) => r);
266
+ }
267
+ } catch (err) {
268
+ this.ctx.log.warn(`Skill relevance judgment failed, returning all: ${err}`);
269
+ }
270
+
271
+ // Fallback: return all candidates
272
+ return candidates.map((_, i) => i);
273
+ }
184
274
  }
185
275
 
186
276
  function makeExcerpt(content: string): string {
@@ -2,6 +2,7 @@ import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import type { SqliteStore } from "../storage/sqlite";
4
4
  import type { RecallEngine } from "../recall/engine";
5
+ import type { Embedder } from "../embedding";
5
6
  import type { Task, Skill, Chunk, PluginContext } from "../types";
6
7
  import { DEFAULTS } from "../types";
7
8
  import { SkillEvaluator } from "./evaluator";
@@ -20,9 +21,10 @@ export class SkillEvolver {
20
21
  private store: SqliteStore,
21
22
  private engine: RecallEngine,
22
23
  private ctx: PluginContext,
24
+ embedder?: Embedder,
23
25
  ) {
24
26
  this.evaluator = new SkillEvaluator(ctx);
25
- this.generator = new SkillGenerator(store, engine, ctx);
27
+ this.generator = new SkillGenerator(store, engine, ctx, embedder);
26
28
  this.upgrader = new SkillUpgrader(store, ctx);
27
29
  this.installer = new SkillInstaller(store, ctx);
28
30
  }
@@ -3,6 +3,7 @@ import * as fs from "fs";
3
3
  import * as path from "path";
4
4
  import type { SqliteStore } from "../storage/sqlite";
5
5
  import type { RecallEngine } from "../recall/engine";
6
+ import type { Embedder } from "../embedding";
6
7
  import type { Chunk, Task, Skill, PluginContext, SummarizerConfig, SkillGenerateOutput } from "../types";
7
8
  import { DEFAULTS } from "../types";
8
9
  import type { CreateEvalResult } from "./evaluator";
@@ -176,13 +177,16 @@ If no references should be extracted, reply with: []`;
176
177
 
177
178
  export class SkillGenerator {
178
179
  private validator: SkillValidator;
180
+ private embedder: Embedder | null = null;
179
181
 
180
182
  constructor(
181
183
  private store: SqliteStore,
182
184
  private engine: RecallEngine,
183
185
  private ctx: PluginContext,
186
+ embedder?: Embedder,
184
187
  ) {
185
188
  this.validator = new SkillValidator(ctx);
189
+ this.embedder = embedder ?? null;
186
190
  }
187
191
 
188
192
  async generate(task: Task, chunks: Chunk[], evalResult: CreateEvalResult): Promise<Skill> {
@@ -270,12 +274,23 @@ export class SkillGenerator {
270
274
  sourceType: "task",
271
275
  dirPath,
272
276
  installed: 0,
277
+ owner: "agent:main",
278
+ visibility: "private",
273
279
  qualityScore: validation.qualityScore,
274
280
  createdAt: now,
275
281
  updatedAt: now,
276
282
  };
277
283
  this.store.insertSkill(skill);
278
284
 
285
+ if (description && this.embedder) {
286
+ try {
287
+ const [descEmb] = await this.embedder.embed([description]);
288
+ if (descEmb) this.store.upsertSkillEmbedding(skillId, descEmb);
289
+ } catch (err) {
290
+ this.ctx.log.warn(`SkillGenerator: embedding for description failed: ${err}`);
291
+ }
292
+ }
293
+
279
294
  this.store.insertSkillVersion({
280
295
  id: uuid(),
281
296
  skillId,