@memtensor/memos-local-openclaw-plugin 1.0.6-beta.1 → 1.0.6-beta.11
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/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +10 -4
- package/dist/client/connector.js.map +1 -1
- package/dist/hub/server.d.ts +2 -0
- package/dist/hub/server.d.ts.map +1 -1
- package/dist/hub/server.js +108 -54
- package/dist/hub/server.js.map +1 -1
- package/dist/ingest/providers/index.d.ts +4 -0
- package/dist/ingest/providers/index.d.ts.map +1 -1
- package/dist/ingest/providers/index.js +20 -4
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/ingest/providers/openai.d.ts +0 -3
- package/dist/ingest/providers/openai.d.ts.map +1 -1
- package/dist/ingest/providers/openai.js +9 -8
- package/dist/ingest/providers/openai.js.map +1 -1
- package/dist/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +35 -43
- package/dist/recall/engine.js.map +1 -1
- package/dist/storage/sqlite.d.ts +13 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +43 -1
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/update-check.d.ts.map +1 -1
- package/dist/update-check.js +2 -7
- package/dist/update-check.js.map +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +115 -27
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +1 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +191 -96
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +208 -253
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/scripts/native-binding.cjs +32 -0
- package/scripts/postinstall.cjs +13 -16
- package/src/client/connector.ts +10 -4
- package/src/hub/server.ts +95 -51
- package/src/ingest/providers/index.ts +26 -18
- package/src/ingest/providers/openai.ts +5 -4
- package/src/recall/engine.ts +34 -41
- package/src/storage/sqlite.ts +43 -1
- package/src/update-check.ts +2 -7
- package/src/viewer/html.ts +115 -27
- package/src/viewer/server.ts +187 -64
package/scripts/postinstall.cjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
const { spawnSync } = require("child_process");
|
|
5
5
|
const path = require("path");
|
|
6
6
|
const fs = require("fs");
|
|
7
|
+
const { validateNativeBinding } = require("./native-binding.cjs");
|
|
7
8
|
|
|
8
9
|
const RESET = "\x1b[0m";
|
|
9
10
|
const GREEN = "\x1b[32m";
|
|
@@ -377,35 +378,31 @@ function findSqliteBinding() {
|
|
|
377
378
|
|
|
378
379
|
function sqliteBindingsExist() {
|
|
379
380
|
const found = findSqliteBinding();
|
|
380
|
-
if (found)
|
|
381
|
-
|
|
382
|
-
|
|
381
|
+
if (!found) return false;
|
|
382
|
+
log(`Native binding found: ${DIM}${found}${RESET}`);
|
|
383
|
+
const status = validateNativeBinding(found);
|
|
384
|
+
if (status.ok) return true;
|
|
385
|
+
if (status.reason === "node-module-version") {
|
|
386
|
+
warn("Native binding exists but was compiled for a different Node.js version.");
|
|
387
|
+
} else {
|
|
388
|
+
warn("Native binding exists but failed to load.");
|
|
383
389
|
}
|
|
390
|
+
warn(`${DIM}${status.message}${RESET}`);
|
|
384
391
|
return false;
|
|
385
392
|
}
|
|
386
393
|
|
|
387
394
|
if (sqliteBindingsExist()) {
|
|
388
395
|
ok("better-sqlite3 is ready.");
|
|
389
396
|
} else {
|
|
390
|
-
warn("better-sqlite3 native bindings
|
|
397
|
+
warn("better-sqlite3 native bindings are missing or not loadable.");
|
|
391
398
|
log(`Searched in: ${DIM}${sqliteModulePath}/build/${RESET}`);
|
|
392
399
|
log("Running: npm rebuild better-sqlite3 (may take 30-60s)...");
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
const startMs = Date.now();
|
|
396
|
-
|
|
397
|
-
const result = spawnSync(npmCmd, ["rebuild", "better-sqlite3"], {
|
|
398
|
-
cwd: pluginDir,
|
|
399
|
-
stdio: "pipe",
|
|
400
|
-
shell: false,
|
|
401
|
-
timeout: 180_000,
|
|
402
|
-
});
|
|
403
400
|
|
|
404
401
|
const startMs = Date.now();
|
|
405
|
-
const result = spawnSync(
|
|
402
|
+
const result = spawnSync(npmCmd, ["rebuild", "better-sqlite3"], {
|
|
406
403
|
cwd: pluginDir,
|
|
407
404
|
stdio: "pipe",
|
|
408
|
-
shell:
|
|
405
|
+
shell: false,
|
|
409
406
|
timeout: 180_000,
|
|
410
407
|
});
|
|
411
408
|
const elapsed = ((Date.now() - startMs) / 1000).toFixed(1);
|
package/src/client/connector.ts
CHANGED
|
@@ -229,22 +229,28 @@ export async function getHubStatus(store: SqliteStore, config: MemosLocalConfig)
|
|
|
229
229
|
body: JSON.stringify({ teamToken, userId: conn.userId }),
|
|
230
230
|
}) as any;
|
|
231
231
|
if (regResult.status === "active" && regResult.userToken) {
|
|
232
|
-
|
|
232
|
+
const updatedConn = {
|
|
233
233
|
...conn,
|
|
234
234
|
hubUrl: normalizeHubUrl(hubAddress),
|
|
235
235
|
userToken: regResult.userToken,
|
|
236
236
|
connectedAt: Date.now(),
|
|
237
237
|
lastKnownStatus: "active",
|
|
238
|
-
}
|
|
238
|
+
};
|
|
239
|
+
store.setClientHubConnection(updatedConn);
|
|
239
240
|
try {
|
|
240
241
|
const me = await hubRequestJson(normalizeHubUrl(hubAddress), regResult.userToken, "/api/v1/hub/me", { method: "GET" }) as any;
|
|
242
|
+
const latestUsername = String(me.username ?? "");
|
|
243
|
+
const latestRole = String(me.role ?? "member") as UserRole;
|
|
244
|
+
if (latestUsername !== conn.username || latestRole !== conn.role) {
|
|
245
|
+
store.setClientHubConnection({ ...updatedConn, username: latestUsername, role: latestRole });
|
|
246
|
+
}
|
|
241
247
|
return {
|
|
242
248
|
connected: true,
|
|
243
249
|
hubUrl: normalizeHubUrl(hubAddress),
|
|
244
250
|
user: {
|
|
245
251
|
id: String(me.id),
|
|
246
|
-
username:
|
|
247
|
-
role:
|
|
252
|
+
username: latestUsername,
|
|
253
|
+
role: latestRole,
|
|
248
254
|
status: String(me.status ?? "active"),
|
|
249
255
|
groups: Array.isArray(me.groups) ? me.groups : [],
|
|
250
256
|
},
|
package/src/hub/server.ts
CHANGED
|
@@ -124,6 +124,8 @@ export class HubServer {
|
|
|
124
124
|
this.initOnlineTracking();
|
|
125
125
|
this.offlineCheckTimer = setInterval(() => this.checkOfflineUsers(), HubServer.OFFLINE_CHECK_INTERVAL_MS);
|
|
126
126
|
|
|
127
|
+
this.backfillMemoryEmbeddings();
|
|
128
|
+
|
|
127
129
|
return `http://127.0.0.1:${hubPort}`;
|
|
128
130
|
}
|
|
129
131
|
|
|
@@ -226,8 +228,47 @@ export class HubServer {
|
|
|
226
228
|
});
|
|
227
229
|
}
|
|
228
230
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
+
private backfillMemoryEmbeddings(): void {
|
|
232
|
+
if (!this.opts.embedder) return;
|
|
233
|
+
try {
|
|
234
|
+
const all = this.opts.store.listHubMemories({ limit: 500 });
|
|
235
|
+
const missing = all.filter(m => {
|
|
236
|
+
try { return !this.opts.store.getHubMemoryEmbedding(m.id); } catch { return true; }
|
|
237
|
+
});
|
|
238
|
+
if (missing.length === 0) return;
|
|
239
|
+
this.opts.log.info(`hub: backfilling embeddings for ${missing.length} hub memories`);
|
|
240
|
+
const texts = missing.map(m => (m.summary || m.content || "").slice(0, 500));
|
|
241
|
+
this.opts.embedder.embed(texts).then((vectors) => {
|
|
242
|
+
let count = 0;
|
|
243
|
+
for (let i = 0; i < vectors.length; i++) {
|
|
244
|
+
if (vectors[i]) {
|
|
245
|
+
this.opts.store.upsertHubMemoryEmbedding(missing[i].id, new Float32Array(vectors[i]));
|
|
246
|
+
count++;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
this.opts.log.info(`hub: backfilled ${count}/${missing.length} memory embeddings`);
|
|
250
|
+
}).catch((err) => {
|
|
251
|
+
this.opts.log.warn(`hub: backfill memory embeddings failed: ${err}`);
|
|
252
|
+
});
|
|
253
|
+
} catch (err) {
|
|
254
|
+
this.opts.log.warn(`hub: backfill memory embeddings error: ${err}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private embedMemoryAsync(memoryId: string, summary: string, content: string): void {
|
|
259
|
+
const embedder = this.opts.embedder;
|
|
260
|
+
if (!embedder) return;
|
|
261
|
+
const text = (summary || content || "").slice(0, 500);
|
|
262
|
+
if (!text) return;
|
|
263
|
+
embedder.embed([text]).then((vectors) => {
|
|
264
|
+
if (vectors[0]) {
|
|
265
|
+
this.opts.store.upsertHubMemoryEmbedding(memoryId, new Float32Array(vectors[0]));
|
|
266
|
+
this.opts.log.info(`hub: embedded shared memory ${memoryId}`);
|
|
267
|
+
}
|
|
268
|
+
}).catch((err) => {
|
|
269
|
+
this.opts.log.warn(`hub: embedding shared memory failed: ${err}`);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
231
272
|
|
|
232
273
|
private async handle(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
233
274
|
const url = new URL(req.url || "/", `http://127.0.0.1:${this.port}`);
|
|
@@ -253,59 +294,64 @@ export class HubServer {
|
|
|
253
294
|
|| (req.headers["x-forwarded-for"] as string)?.split(",")[0]?.trim()
|
|
254
295
|
|| req.socket.remoteAddress || "";
|
|
255
296
|
const identityKey = typeof body.identityKey === "string" ? body.identityKey.trim() : "";
|
|
297
|
+
const dryRun = body.dryRun === true;
|
|
256
298
|
|
|
257
|
-
|
|
299
|
+
const identityMatch = identityKey
|
|
258
300
|
? this.userManager.findByIdentityKey(identityKey)
|
|
259
301
|
: null;
|
|
260
|
-
if (!existingUser) {
|
|
261
|
-
const existingUsers = this.opts.store.listHubUsers();
|
|
262
|
-
existingUser = existingUsers.find(u => u.username === username && u.status !== "left" && u.status !== "removed") ?? null;
|
|
263
|
-
}
|
|
264
302
|
|
|
265
|
-
if (
|
|
266
|
-
|
|
303
|
+
if (identityMatch) {
|
|
304
|
+
if (!dryRun) {
|
|
305
|
+
try { this.opts.store.updateHubUserActivity(identityMatch.id, joinIp); } catch { /* best-effort */ }
|
|
306
|
+
}
|
|
267
307
|
|
|
268
|
-
if (
|
|
308
|
+
if (identityMatch.status === "active") {
|
|
309
|
+
if (dryRun) return this.json(res, 200, { status: "active", dryRun: true });
|
|
269
310
|
const token = issueUserToken(
|
|
270
|
-
{ userId:
|
|
311
|
+
{ userId: identityMatch.id, username: identityMatch.username, role: identityMatch.role, status: "active" },
|
|
271
312
|
this.authSecret,
|
|
272
313
|
);
|
|
273
|
-
this.userManager.approveUser(
|
|
274
|
-
|
|
275
|
-
this.opts.store.upsertHubUser({ ...existingUser, identityKey });
|
|
276
|
-
}
|
|
277
|
-
return this.json(res, 200, { status: "active", userId: existingUser.id, userToken: token, identityKey: existingUser.identityKey || identityKey });
|
|
314
|
+
this.userManager.approveUser(identityMatch.id, token);
|
|
315
|
+
return this.json(res, 200, { status: "active", userId: identityMatch.id, userToken: token, identityKey: identityMatch.identityKey || identityKey });
|
|
278
316
|
}
|
|
279
|
-
if (
|
|
280
|
-
this.
|
|
281
|
-
|
|
317
|
+
if (identityMatch.status === "pending") {
|
|
318
|
+
if (dryRun) return this.json(res, 200, { status: "pending", dryRun: true });
|
|
319
|
+
this.notifyAdmins("user_join_request", "user", identityMatch.username, "", { dedup: true });
|
|
320
|
+
return this.json(res, 200, { status: "pending", userId: identityMatch.id, identityKey: identityMatch.identityKey || identityKey });
|
|
282
321
|
}
|
|
283
|
-
if (
|
|
322
|
+
if (identityMatch.status === "rejected") {
|
|
323
|
+
if (dryRun) return this.json(res, 200, { status: "rejected", dryRun: true });
|
|
284
324
|
if (body.reapply === true) {
|
|
285
|
-
this.userManager.resetToPending(
|
|
286
|
-
this.notifyAdmins("user_join_request", "user", username, "");
|
|
287
|
-
this.opts.log.info(`Hub: rejected user "${username}" (${
|
|
288
|
-
return this.json(res, 200, { status: "pending", userId:
|
|
325
|
+
this.userManager.resetToPending(identityMatch.id);
|
|
326
|
+
this.notifyAdmins("user_join_request", "user", identityMatch.username, "");
|
|
327
|
+
this.opts.log.info(`Hub: rejected user "${identityMatch.username}" (${identityMatch.id}) re-applied, reset to pending`);
|
|
328
|
+
return this.json(res, 200, { status: "pending", userId: identityMatch.id, identityKey: identityMatch.identityKey || identityKey });
|
|
289
329
|
}
|
|
290
|
-
return this.json(res, 200, { status: "rejected", userId:
|
|
291
|
-
}
|
|
292
|
-
if (existingUser.status === "removed") {
|
|
293
|
-
this.userManager.rejoinUser(existingUser.id);
|
|
294
|
-
this.notifyAdmins("user_join_request", "user", username, "", { dedup: true });
|
|
295
|
-
this.opts.log.info(`Hub: removed user "${username}" (${existingUser.id}) re-applied via rejoin, reset to pending`);
|
|
296
|
-
return this.json(res, 200, { status: "pending", userId: existingUser.id, identityKey: existingUser.identityKey || identityKey });
|
|
330
|
+
return this.json(res, 200, { status: "rejected", userId: identityMatch.id });
|
|
297
331
|
}
|
|
298
|
-
if (
|
|
299
|
-
this.
|
|
300
|
-
this.
|
|
301
|
-
this.
|
|
302
|
-
|
|
332
|
+
if (identityMatch.status === "removed" || identityMatch.status === "left") {
|
|
333
|
+
if (dryRun) return this.json(res, 200, { status: "can_rejoin", dryRun: true });
|
|
334
|
+
this.userManager.rejoinUser(identityMatch.id);
|
|
335
|
+
this.notifyAdmins("user_join_request", "user", identityMatch.username, "", { dedup: true });
|
|
336
|
+
this.opts.log.info(`Hub: ${identityMatch.status} user "${identityMatch.username}" (${identityMatch.id}) re-applied via rejoin, reset to pending`);
|
|
337
|
+
return this.json(res, 200, { status: "pending", userId: identityMatch.id, identityKey: identityMatch.identityKey || identityKey });
|
|
303
338
|
}
|
|
304
|
-
if (
|
|
305
|
-
return this.json(res, 200, { status: "blocked", userId:
|
|
339
|
+
if (identityMatch.status === "blocked") {
|
|
340
|
+
return this.json(res, 200, { status: "blocked", userId: identityMatch.id });
|
|
306
341
|
}
|
|
307
342
|
}
|
|
308
343
|
|
|
344
|
+
const existingUsers = this.opts.store.listHubUsers();
|
|
345
|
+
const nameConflict = existingUsers.find(u => u.username === username);
|
|
346
|
+
if (nameConflict) {
|
|
347
|
+
this.opts.log.info(`Hub: join rejected — username "${username}" already taken by user ${nameConflict.id} (status=${nameConflict.status})`);
|
|
348
|
+
return this.json(res, 409, { error: "username_taken", message: `Username "${username}" is already in use. Please choose a different nickname.` });
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (dryRun) {
|
|
352
|
+
return this.json(res, 200, { status: "ok", dryRun: true });
|
|
353
|
+
}
|
|
354
|
+
|
|
309
355
|
const generatedIdentityKey = identityKey || randomUUID();
|
|
310
356
|
const user = this.userManager.createPendingUser({
|
|
311
357
|
username,
|
|
@@ -534,6 +580,12 @@ export class HubServer {
|
|
|
534
580
|
this.authState.bootstrapAdminToken = newToken;
|
|
535
581
|
this.saveAuthState();
|
|
536
582
|
}
|
|
583
|
+
try {
|
|
584
|
+
this.opts.store.insertHubNotification({
|
|
585
|
+
id: randomUUID(), userId, type: "username_renamed",
|
|
586
|
+
resource: "user", title: `Your nickname has been changed from "${user.username}" to "${newUsername}" by the admin.`,
|
|
587
|
+
});
|
|
588
|
+
} catch { /* best-effort */ }
|
|
537
589
|
this.opts.log.info(`Hub: admin "${auth.userId}" renamed user "${userId}" to "${newUsername}"`);
|
|
538
590
|
return this.json(res, 200, { ok: true, username: newUsername });
|
|
539
591
|
}
|
|
@@ -615,7 +667,7 @@ export class HubServer {
|
|
|
615
667
|
createdAt: existing?.createdAt ?? now,
|
|
616
668
|
updatedAt: now,
|
|
617
669
|
});
|
|
618
|
-
|
|
670
|
+
this.embedMemoryAsync(memoryId, String(m.summary || ""), String(m.content || ""));
|
|
619
671
|
if (!existing) {
|
|
620
672
|
this.notifyAdmins("resource_shared", "memory", String(m.summary || m.content?.slice(0, 60) || memoryId), auth.userId);
|
|
621
673
|
}
|
|
@@ -680,18 +732,10 @@ export class HubServer {
|
|
|
680
732
|
};
|
|
681
733
|
for (const e of allEmb) scored.push({ id: e.chunkId, score: cosineSim(e.vector, queryVec) });
|
|
682
734
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
const memVecs = await this.opts.embedder.embed(memTexts);
|
|
688
|
-
memFtsHits.forEach(({ hit }, i) => {
|
|
689
|
-
if (memVecs[i]) {
|
|
690
|
-
scored.push({ id: hit.id, score: cosineSim(new Float32Array(memVecs[i]), queryVec) });
|
|
691
|
-
memoryIdSet.add(hit.id);
|
|
692
|
-
}
|
|
693
|
-
});
|
|
694
|
-
} catch { /* best-effort */ }
|
|
735
|
+
const memEmb = this.opts.store.getVisibleHubMemoryEmbeddings(auth.userId);
|
|
736
|
+
for (const e of memEmb) {
|
|
737
|
+
scored.push({ id: e.memoryId, score: cosineSim(e.vector, queryVec) });
|
|
738
|
+
memoryIdSet.add(e.memoryId);
|
|
695
739
|
}
|
|
696
740
|
|
|
697
741
|
scored.sort((a, b) => b.score - a.score);
|
|
@@ -1,20 +1,7 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import type { SummarizerConfig, SummaryProvider, Logger, OpenClawAPI } from "../../types";
|
|
4
|
-
import {
|
|
5
|
-
summarizeOpenAI,
|
|
6
|
-
summarizeTaskOpenAI,
|
|
7
|
-
generateTaskTitleOpenAI,
|
|
8
|
-
judgeNewTopicOpenAI,
|
|
9
|
-
filterRelevantOpenAI,
|
|
10
|
-
judgeDedupOpenAI,
|
|
11
|
-
parseFilterResult,
|
|
12
|
-
parseDedupResult,
|
|
13
|
-
TASK_SUMMARY_PROMPT,
|
|
14
|
-
TOPIC_JUDGE_PROMPT,
|
|
15
|
-
FILTER_RELEVANT_PROMPT,
|
|
16
|
-
DEDUP_JUDGE_PROMPT,
|
|
17
|
-
} from "./openai";
|
|
4
|
+
import { summarizeOpenAI, summarizeTaskOpenAI, generateTaskTitleOpenAI, judgeNewTopicOpenAI, filterRelevantOpenAI, judgeDedupOpenAI, parseFilterResult, parseDedupResult } from "./openai";
|
|
18
5
|
import type { FilterResult, DedupResult } from "./openai";
|
|
19
6
|
export type { FilterResult, DedupResult } from "./openai";
|
|
20
7
|
import { summarizeAnthropic, summarizeTaskAnthropic, generateTaskTitleAnthropic, judgeNewTopicAnthropic, filterRelevantAnthropic, judgeDedupAnthropic } from "./anthropic";
|
|
@@ -342,6 +329,27 @@ export class Summarizer {
|
|
|
342
329
|
return this.strongCfg;
|
|
343
330
|
}
|
|
344
331
|
|
|
332
|
+
// ─── OpenClaw Prompts ───
|
|
333
|
+
|
|
334
|
+
static readonly OPENCLAW_TOPIC_JUDGE_PROMPT = `You are a conversation topic change detector.
|
|
335
|
+
Given a CURRENT CONVERSATION SUMMARY and a NEW USER MESSAGE, decide: has the user started a COMPLETELY NEW topic that is unrelated to the current conversation?
|
|
336
|
+
Reply with a single word: "NEW" if topic changed, "SAME" if it continues.`;
|
|
337
|
+
|
|
338
|
+
static readonly OPENCLAW_FILTER_RELEVANT_PROMPT = `You are a memory relevance judge.
|
|
339
|
+
Given a QUERY and CANDIDATE memories, decide: does each candidate help answer the query?
|
|
340
|
+
RULES:
|
|
341
|
+
1. Include candidates whose content provides useful facts/context for the query.
|
|
342
|
+
2. Exclude candidates that merely share a topic but contain no useful information.
|
|
343
|
+
3. DEDUPLICATION: When multiple candidates convey the same or very similar information, keep ONLY the most complete one and exclude the rest.
|
|
344
|
+
4. If none help, return {"relevant":[],"sufficient":false}.
|
|
345
|
+
OUTPUT — JSON only: {"relevant":[1,3],"sufficient":true}`;
|
|
346
|
+
|
|
347
|
+
static readonly OPENCLAW_DEDUP_JUDGE_PROMPT = `You are a memory deduplication system.
|
|
348
|
+
Given a NEW memory summary and EXISTING candidates, decide if the new memory duplicates any existing one.
|
|
349
|
+
Reply with JSON: {"action":"MERGE","mergeTarget":2,"reason":"..."} or {"action":"NEW","reason":"..."}`;
|
|
350
|
+
|
|
351
|
+
static readonly OPENCLAW_TASK_SUMMARY_PROMPT = `Summarize the following task conversation into a structured report. Preserve key decisions, code, commands, and outcomes. Use the same language as the input.`;
|
|
352
|
+
|
|
345
353
|
// ─── OpenClaw API Implementation ───
|
|
346
354
|
|
|
347
355
|
private requireOpenClawAPI(): void {
|
|
@@ -373,7 +381,7 @@ export class Summarizer {
|
|
|
373
381
|
private async summarizeTaskOpenClaw(text: string): Promise<string> {
|
|
374
382
|
this.requireOpenClawAPI();
|
|
375
383
|
const prompt = [
|
|
376
|
-
|
|
384
|
+
Summarizer.OPENCLAW_TASK_SUMMARY_PROMPT,
|
|
377
385
|
``,
|
|
378
386
|
text,
|
|
379
387
|
].join("\n");
|
|
@@ -391,7 +399,7 @@ export class Summarizer {
|
|
|
391
399
|
private async judgeNewTopicOpenClaw(currentContext: string, newMessage: string): Promise<boolean> {
|
|
392
400
|
this.requireOpenClawAPI();
|
|
393
401
|
const prompt = [
|
|
394
|
-
|
|
402
|
+
Summarizer.OPENCLAW_TOPIC_JUDGE_PROMPT,
|
|
395
403
|
``,
|
|
396
404
|
`CURRENT CONVERSATION SUMMARY:`,
|
|
397
405
|
currentContext,
|
|
@@ -422,7 +430,7 @@ export class Summarizer {
|
|
|
422
430
|
.join("\n");
|
|
423
431
|
|
|
424
432
|
const prompt = [
|
|
425
|
-
|
|
433
|
+
Summarizer.OPENCLAW_FILTER_RELEVANT_PROMPT,
|
|
426
434
|
``,
|
|
427
435
|
`QUERY: ${query}`,
|
|
428
436
|
``,
|
|
@@ -450,7 +458,7 @@ export class Summarizer {
|
|
|
450
458
|
.join("\n");
|
|
451
459
|
|
|
452
460
|
const prompt = [
|
|
453
|
-
|
|
461
|
+
Summarizer.OPENCLAW_DEDUP_JUDGE_PROMPT,
|
|
454
462
|
``,
|
|
455
463
|
`NEW MEMORY:`,
|
|
456
464
|
newSummary,
|
|
@@ -16,7 +16,7 @@ Requirements:
|
|
|
16
16
|
- Non-Chinese: 5-15 words (aim for 8-12)
|
|
17
17
|
- Output title only`;
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
const TASK_SUMMARY_PROMPT = `You create a DETAILED task summary from a multi-turn conversation. This summary will be the ONLY record of this conversation, so it must preserve ALL important information.
|
|
20
20
|
|
|
21
21
|
## LANGUAGE RULE (HIGHEST PRIORITY)
|
|
22
22
|
Detect the PRIMARY language of the user's messages. If most user messages are Chinese, ALL output (title, goal, steps, result, details) MUST be in Chinese. If English, output in English. NEVER mix. This rule overrides everything below.
|
|
@@ -178,7 +178,7 @@ export async function summarizeOpenAI(
|
|
|
178
178
|
return json.choices[0]?.message?.content?.trim() ?? "";
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
|
|
181
|
+
const TOPIC_JUDGE_PROMPT = `You are a conversation topic boundary detector. Given the CURRENT task context and a NEW user message, decide if the new message belongs to the SAME task or starts a NEW one.
|
|
182
182
|
|
|
183
183
|
Answer ONLY "NEW" or "SAME".
|
|
184
184
|
|
|
@@ -246,7 +246,7 @@ export async function judgeNewTopicOpenAI(
|
|
|
246
246
|
return answer.startsWith("NEW");
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
|
|
249
|
+
const FILTER_RELEVANT_PROMPT = `You are a memory relevance judge.
|
|
250
250
|
|
|
251
251
|
Given a QUERY and CANDIDATE memories, decide: does each candidate's content contain information that would HELP ANSWER the query?
|
|
252
252
|
|
|
@@ -258,10 +258,11 @@ RULES:
|
|
|
258
258
|
1. A candidate is relevant if its content provides facts, context, or data that directly supports answering the query.
|
|
259
259
|
2. A candidate that merely shares the same broad topic/domain but contains NO useful information for answering is NOT relevant.
|
|
260
260
|
3. If NO candidate can help answer the query, return {"relevant":[],"sufficient":false} — do NOT force-pick the "least irrelevant" one.
|
|
261
|
+
4. DEDUPLICATION: When multiple candidates convey the same or very similar information, keep ONLY the most complete/detailed one and exclude the rest. Do NOT return near-duplicate snippets.
|
|
261
262
|
|
|
262
263
|
OUTPUT — JSON only:
|
|
263
264
|
{"relevant":[1,3],"sufficient":true}
|
|
264
|
-
- "relevant": candidate numbers whose content helps answer the query. [] if none can help.
|
|
265
|
+
- "relevant": candidate numbers whose content helps answer the query. [] if none can help. Duplicates removed — only unique information.
|
|
265
266
|
- "sufficient": true only if the selected memories fully answer the query.`;
|
|
266
267
|
|
|
267
268
|
export interface FilterResult {
|
package/src/recall/engine.ts
CHANGED
|
@@ -62,10 +62,20 @@ export class RecallEngine {
|
|
|
62
62
|
|
|
63
63
|
// Step 1b: Pattern search (LIKE-based) as fallback for short terms that
|
|
64
64
|
// trigram FTS cannot match (trigram requires >= 3 chars).
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
// For CJK text without spaces, extract bigrams (2-char sliding windows)
|
|
66
|
+
// so that queries like "唐波是谁" produce ["唐波", "波是", "是谁"].
|
|
67
|
+
const cleaned = query.replace(/[."""(){}[\]*:^~!@#$%&\\/<>,;'`??。,!、:""''()【】《》]/g, " ");
|
|
68
|
+
const spaceSplit = cleaned.split(/\s+/).filter((t) => t.length === 2);
|
|
69
|
+
const cjkBigrams: string[] = [];
|
|
70
|
+
const cjkRuns = cleaned.match(/[\u4e00-\u9fff\u3400-\u4dbf\uF900-\uFAFF]{2,}/g);
|
|
71
|
+
if (cjkRuns) {
|
|
72
|
+
for (const run of cjkRuns) {
|
|
73
|
+
for (let i = 0; i <= run.length - 2; i++) {
|
|
74
|
+
cjkBigrams.push(run.slice(i, i + 2));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const shortTerms = [...new Set([...spaceSplit, ...cjkBigrams])];
|
|
69
79
|
const patternHits = shortTerms.length > 0
|
|
70
80
|
? this.store.patternSearch(shortTerms, { limit: candidatePool })
|
|
71
81
|
: [];
|
|
@@ -74,58 +84,41 @@ export class RecallEngine {
|
|
|
74
84
|
score: 1 / (i + 1),
|
|
75
85
|
}));
|
|
76
86
|
|
|
77
|
-
// Step 1c: Hub memories —
|
|
78
|
-
// Stage 1: FTS + pattern to get candidates.
|
|
79
|
-
// Stage 2: embed candidates on-the-fly + cosine rerank.
|
|
87
|
+
// Step 1c: Hub memories — FTS + pattern + cached embeddings (same strategy as chunks/skills).
|
|
80
88
|
let hubMemFtsRanked: Array<{ id: string; score: number }> = [];
|
|
81
89
|
let hubMemVecRanked: Array<{ id: string; score: number }> = [];
|
|
82
90
|
let hubMemPatternRanked: Array<{ id: string; score: number }> = [];
|
|
83
91
|
if (query && this.ctx.config.sharing?.enabled && this.ctx.config.sharing.role === "hub") {
|
|
84
|
-
// Stage 1: cheap text retrieval
|
|
85
|
-
const hubCandidateTexts = new Map<string, string>();
|
|
86
92
|
try {
|
|
87
93
|
const hubFtsHits = this.store.searchHubMemories(query, { maxResults: candidatePool });
|
|
88
|
-
hubMemFtsRanked = hubFtsHits.map(({ hit }, i) => {
|
|
89
|
-
hubCandidateTexts.set(hit.id, (hit.summary || hit.content || "").slice(0, 500));
|
|
90
|
-
return { id: `hubmem:${hit.id}`, score: 1 / (i + 1) };
|
|
91
|
-
});
|
|
94
|
+
hubMemFtsRanked = hubFtsHits.map(({ hit }, i) => ({ id: `hubmem:${hit.id}`, score: 1 / (i + 1) }));
|
|
92
95
|
} catch { /* hub_memories table may not exist */ }
|
|
93
96
|
if (shortTerms.length > 0) {
|
|
94
97
|
try {
|
|
95
98
|
const hubPatternHits = this.store.hubMemoryPatternSearch(shortTerms, { limit: candidatePool });
|
|
96
|
-
hubMemPatternRanked = hubPatternHits.map((h, i) => {
|
|
97
|
-
hubCandidateTexts.set(h.memoryId, (h.content || "").slice(0, 500));
|
|
98
|
-
return { id: `hubmem:${h.memoryId}`, score: 1 / (i + 1) };
|
|
99
|
-
});
|
|
99
|
+
hubMemPatternRanked = hubPatternHits.map((h, i) => ({ id: `hubmem:${h.memoryId}`, score: 1 / (i + 1) }));
|
|
100
100
|
} catch { /* best-effort */ }
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (!vecs[j]) continue;
|
|
114
|
-
const v = vecs[j];
|
|
115
|
-
let dot = 0, nA = 0, nB = 0;
|
|
116
|
-
for (let i = 0; i < qv.length && i < v.length; i++) {
|
|
117
|
-
dot += qv[i] * v[i]; nA += qv[i] * qv[i]; nB += v[i] * v[i];
|
|
118
|
-
}
|
|
119
|
-
const sim = nA > 0 && nB > 0 ? dot / (Math.sqrt(nA) * Math.sqrt(nB)) : 0;
|
|
120
|
-
if (sim > 0.3) {
|
|
121
|
-
scored.push({ id: `hubmem:${ids[j]}`, score: sim });
|
|
122
|
-
}
|
|
103
|
+
try {
|
|
104
|
+
const qv = await this.embedder.embedQuery(query).catch(() => null);
|
|
105
|
+
if (qv) {
|
|
106
|
+
const memEmbs = this.store.getVisibleHubMemoryEmbeddings("__hub__");
|
|
107
|
+
const scored: Array<{ id: string; score: number }> = [];
|
|
108
|
+
for (const e of memEmbs) {
|
|
109
|
+
let dot = 0, nA = 0, nB = 0;
|
|
110
|
+
const len = Math.min(qv.length, e.vector.length);
|
|
111
|
+
for (let i = 0; i < len; i++) {
|
|
112
|
+
dot += qv[i] * e.vector[i]; nA += qv[i] * qv[i]; nB += e.vector[i] * e.vector[i];
|
|
123
113
|
}
|
|
124
|
-
|
|
125
|
-
|
|
114
|
+
const sim = nA > 0 && nB > 0 ? dot / (Math.sqrt(nA) * Math.sqrt(nB)) : 0;
|
|
115
|
+
if (sim > 0.3) scored.push({ id: `hubmem:${e.memoryId}`, score: sim });
|
|
126
116
|
}
|
|
127
|
-
|
|
128
|
-
|
|
117
|
+
scored.sort((a, b) => b.score - a.score);
|
|
118
|
+
hubMemVecRanked = scored.slice(0, candidatePool);
|
|
119
|
+
}
|
|
120
|
+
} catch { /* best-effort */ }
|
|
121
|
+
|
|
129
122
|
const hubTotal = hubMemFtsRanked.length + hubMemVecRanked.length + hubMemPatternRanked.length;
|
|
130
123
|
if (hubTotal > 0) {
|
|
131
124
|
this.ctx.log.debug(`recall: hub_memories candidates: fts=${hubMemFtsRanked.length}, vec=${hubMemVecRanked.length}, pattern=${hubMemPatternRanked.length}`);
|
package/src/storage/sqlite.ts
CHANGED
|
@@ -1005,7 +1005,12 @@ export class SqliteStore {
|
|
|
1005
1005
|
CREATE INDEX IF NOT EXISTS idx_hub_memories_visibility ON hub_memories(visibility);
|
|
1006
1006
|
CREATE INDEX IF NOT EXISTS idx_hub_memories_group ON hub_memories(group_id);
|
|
1007
1007
|
|
|
1008
|
-
|
|
1008
|
+
CREATE TABLE IF NOT EXISTS hub_memory_embeddings (
|
|
1009
|
+
memory_id TEXT PRIMARY KEY REFERENCES hub_memories(id) ON DELETE CASCADE,
|
|
1010
|
+
vector BLOB NOT NULL,
|
|
1011
|
+
dimensions INTEGER NOT NULL,
|
|
1012
|
+
updated_at INTEGER NOT NULL
|
|
1013
|
+
);
|
|
1009
1014
|
|
|
1010
1015
|
CREATE VIRTUAL TABLE IF NOT EXISTS hub_memories_fts USING fts5(
|
|
1011
1016
|
summary,
|
|
@@ -1252,6 +1257,13 @@ export class SqliteStore {
|
|
|
1252
1257
|
} catch { return []; }
|
|
1253
1258
|
}
|
|
1254
1259
|
|
|
1260
|
+
listHubMemories(opts: { limit?: number } = {}): Array<{ id: string; summary?: string; content?: string }> {
|
|
1261
|
+
const limit = opts.limit ?? 200;
|
|
1262
|
+
try {
|
|
1263
|
+
return this.db.prepare("SELECT id, summary, content FROM hub_memories ORDER BY created_at DESC LIMIT ?").all(limit) as Array<{ id: string; summary?: string; content?: string }>;
|
|
1264
|
+
} catch { return []; }
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1255
1267
|
// ─── Vector Search ───
|
|
1256
1268
|
|
|
1257
1269
|
getAllEmbeddings(ownerFilter?: string[]): Array<{ chunkId: string; vector: number[] }> {
|
|
@@ -2194,6 +2206,36 @@ export class SqliteStore {
|
|
|
2194
2206
|
}));
|
|
2195
2207
|
}
|
|
2196
2208
|
|
|
2209
|
+
upsertHubMemoryEmbedding(memoryId: string, vector: Float32Array): void {
|
|
2210
|
+
const buf = Buffer.from(vector.buffer, vector.byteOffset, vector.byteLength);
|
|
2211
|
+
this.db.prepare(`
|
|
2212
|
+
INSERT INTO hub_memory_embeddings (memory_id, vector, dimensions, updated_at)
|
|
2213
|
+
VALUES (?, ?, ?, ?)
|
|
2214
|
+
ON CONFLICT(memory_id) DO UPDATE SET vector = excluded.vector, dimensions = excluded.dimensions, updated_at = excluded.updated_at
|
|
2215
|
+
`).run(memoryId, buf, vector.length, Date.now());
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
getHubMemoryEmbedding(memoryId: string): Float32Array | null {
|
|
2219
|
+
const row = this.db.prepare('SELECT vector, dimensions FROM hub_memory_embeddings WHERE memory_id = ?').get(memoryId) as { vector: Buffer; dimensions: number } | undefined;
|
|
2220
|
+
if (!row) return null;
|
|
2221
|
+
return new Float32Array(row.vector.buffer, row.vector.byteOffset, row.dimensions);
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
getVisibleHubMemoryEmbeddings(userId: string): Array<{ memoryId: string; vector: Float32Array }> {
|
|
2225
|
+
const rows = this.db.prepare(`
|
|
2226
|
+
SELECT hme.memory_id, hme.vector, hme.dimensions
|
|
2227
|
+
FROM hub_memory_embeddings hme
|
|
2228
|
+
JOIN hub_memories hm ON hm.id = hme.memory_id
|
|
2229
|
+
WHERE hm.visibility = 'public'
|
|
2230
|
+
OR hm.source_user_id = ?
|
|
2231
|
+
OR EXISTS (SELECT 1 FROM hub_group_members gm WHERE gm.group_id = hm.group_id AND gm.user_id = ?)
|
|
2232
|
+
`).all(userId, userId) as Array<{ memory_id: string; vector: Buffer; dimensions: number }>;
|
|
2233
|
+
return rows.map(r => ({
|
|
2234
|
+
memoryId: r.memory_id,
|
|
2235
|
+
vector: new Float32Array(r.vector.buffer, r.vector.byteOffset, r.dimensions),
|
|
2236
|
+
}));
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2197
2239
|
searchHubChunks(query: string, options?: { userId?: string; maxResults?: number }): Array<{ hit: HubSearchRow; rank: number }> {
|
|
2198
2240
|
const limit = options?.maxResults ?? 10;
|
|
2199
2241
|
const userId = options?.userId ?? "";
|
package/src/update-check.ts
CHANGED
|
@@ -52,26 +52,21 @@ export async function computeUpdateCheck(
|
|
|
52
52
|
|
|
53
53
|
if (onBeta) {
|
|
54
54
|
channel = "beta";
|
|
55
|
-
// Beta users: only compare against beta tag; never suggest "updating" to stable via gt confusion.
|
|
56
55
|
if (betaTag && semver.valid(betaTag) && semver.gt(betaTag, current)) {
|
|
57
56
|
updateAvailable = true;
|
|
58
57
|
targetVersion = betaTag;
|
|
59
|
-
installCommand = `openclaw plugins install ${packageName}@beta`;
|
|
60
58
|
} else {
|
|
61
59
|
targetVersion = betaTag && semver.valid(betaTag) ? betaTag : current;
|
|
62
|
-
if (betaTag && semver.valid(betaTag) && semver.eq(betaTag, current)) {
|
|
63
|
-
installCommand = `openclaw plugins install ${packageName}@beta`;
|
|
64
|
-
}
|
|
65
60
|
}
|
|
61
|
+
installCommand = `openclaw plugins install ${packageName}@${targetVersion}`;
|
|
66
62
|
} else {
|
|
67
|
-
// Stable users: compare against latest only.
|
|
68
63
|
if (latestTag && semver.valid(latestTag) && semver.gt(latestTag, current)) {
|
|
69
64
|
updateAvailable = true;
|
|
70
65
|
targetVersion = latestTag;
|
|
71
|
-
installCommand = `openclaw plugins install ${packageName}`;
|
|
72
66
|
} else {
|
|
73
67
|
targetVersion = latestTag && semver.valid(latestTag) ? latestTag : current;
|
|
74
68
|
}
|
|
69
|
+
installCommand = `openclaw plugins install ${packageName}@${targetVersion}`;
|
|
75
70
|
}
|
|
76
71
|
|
|
77
72
|
// Beta user + stable exists on latest: optional hint to switch to stable (not counted as "update").
|