@memtensor/memos-local-openclaw-plugin 1.0.4-beta.8 → 1.0.4-beta.9
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 +45 -4
- package/dist/client/connector.js.map +1 -1
- package/dist/hub/server.d.ts.map +1 -1
- package/dist/hub/server.js +12 -4
- package/dist/hub/server.js.map +1 -1
- package/dist/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +78 -1
- package/dist/recall/engine.js.map +1 -1
- package/dist/storage/sqlite.d.ts +1 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +5 -0
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +282 -114
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +2 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +119 -4
- package/dist/viewer/server.js.map +1 -1
- package/package.json +1 -1
- package/src/client/connector.ts +46 -4
- package/src/hub/server.ts +12 -4
- package/src/recall/engine.ts +73 -1
- package/src/storage/sqlite.ts +8 -0
- package/src/viewer/html.ts +282 -114
- package/src/viewer/server.ts +117 -4
package/package.json
CHANGED
package/src/client/connector.ts
CHANGED
|
@@ -34,6 +34,41 @@ export async function connectToHub(store: SqliteStore, config: MemosLocalConfig,
|
|
|
34
34
|
|
|
35
35
|
if (!userToken && config.sharing?.client?.teamToken) {
|
|
36
36
|
if (!log) throw new Error("hub client connection is not configured (no userToken, has teamToken but no logger for auto-join)");
|
|
37
|
+
|
|
38
|
+
// If DB has a pending connection (userId exists, no token), check registration-status first
|
|
39
|
+
const persisted = store.getClientHubConnection();
|
|
40
|
+
if (persisted?.userId && !persisted.userToken && hubAddress) {
|
|
41
|
+
const hubUrl = normalizeHubUrl(hubAddress);
|
|
42
|
+
const teamToken = config.sharing.client!.teamToken!;
|
|
43
|
+
try {
|
|
44
|
+
const result = await hubRequestJson(hubUrl, "", "/api/v1/hub/registration-status", {
|
|
45
|
+
method: "POST",
|
|
46
|
+
body: JSON.stringify({ teamToken, userId: persisted.userId }),
|
|
47
|
+
}) as any;
|
|
48
|
+
if (result.status === "active" && result.userToken) {
|
|
49
|
+
log.info(`Pending user approved! Connecting with token. userId=${persisted.userId}`);
|
|
50
|
+
store.setClientHubConnection({
|
|
51
|
+
hubUrl,
|
|
52
|
+
userId: persisted.userId,
|
|
53
|
+
username: persisted.username || "",
|
|
54
|
+
userToken: result.userToken,
|
|
55
|
+
role: "member",
|
|
56
|
+
connectedAt: Date.now(),
|
|
57
|
+
});
|
|
58
|
+
return store.getClientHubConnection()!;
|
|
59
|
+
}
|
|
60
|
+
if (result.status === "pending") {
|
|
61
|
+
throw new PendingApprovalError(persisted.userId);
|
|
62
|
+
}
|
|
63
|
+
if (result.status === "rejected") {
|
|
64
|
+
throw new Error("Join request was rejected by the Hub admin.");
|
|
65
|
+
}
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (err instanceof PendingApprovalError) throw err;
|
|
68
|
+
log.warn(`registration-status check failed, falling back to autoJoinHub: ${err}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
37
72
|
return autoJoinHub(store, config, log);
|
|
38
73
|
}
|
|
39
74
|
|
|
@@ -56,16 +91,23 @@ export async function connectToHub(store: SqliteStore, config: MemosLocalConfig,
|
|
|
56
91
|
|
|
57
92
|
export async function getHubStatus(store: SqliteStore, config: MemosLocalConfig): Promise<HubStatusInfo> {
|
|
58
93
|
const conn = store.getClientHubConnection();
|
|
59
|
-
const
|
|
94
|
+
const configHubAddress = config.sharing?.client?.hubAddress || "";
|
|
95
|
+
const hubAddress = conn?.hubUrl || (configHubAddress ? normalizeHubUrl(configHubAddress) : "");
|
|
60
96
|
const userToken = conn?.userToken || config.sharing?.client?.userToken || "";
|
|
61
97
|
|
|
98
|
+
// If DB has a connection to a different Hub than config, the DB data is stale
|
|
99
|
+
if (conn && configHubAddress && conn.hubUrl && normalizeHubUrl(configHubAddress) !== conn.hubUrl) {
|
|
100
|
+
store.clearClientHubConnection();
|
|
101
|
+
return { connected: false, user: null };
|
|
102
|
+
}
|
|
103
|
+
|
|
62
104
|
if (conn && conn.userId && (!userToken || userToken === "")) {
|
|
63
105
|
const teamToken = config.sharing?.client?.teamToken ?? "";
|
|
64
106
|
if (hubAddress && teamToken) {
|
|
65
107
|
try {
|
|
66
|
-
const result = await hubRequestJson(normalizeHubUrl(hubAddress), "", "/api/v1/hub/
|
|
108
|
+
const result = await hubRequestJson(normalizeHubUrl(hubAddress), "", "/api/v1/hub/registration-status", {
|
|
67
109
|
method: "POST",
|
|
68
|
-
body: JSON.stringify({ teamToken,
|
|
110
|
+
body: JSON.stringify({ teamToken, userId: conn.userId }),
|
|
69
111
|
}) as any;
|
|
70
112
|
if (result.status === "pending") {
|
|
71
113
|
return {
|
|
@@ -82,7 +124,7 @@ export async function getHubStatus(store: SqliteStore, config: MemosLocalConfig)
|
|
|
82
124
|
if (result.status === "active" && result.userToken) {
|
|
83
125
|
store.setClientHubConnection({
|
|
84
126
|
hubUrl: normalizeHubUrl(hubAddress),
|
|
85
|
-
userId:
|
|
127
|
+
userId: conn.userId,
|
|
86
128
|
username: conn.username || "",
|
|
87
129
|
userToken: result.userToken,
|
|
88
130
|
role: "member",
|
package/src/hub/server.ts
CHANGED
|
@@ -218,12 +218,17 @@ export class HubServer {
|
|
|
218
218
|
return this.json(res, 200, { status: "active", userId: existingUser.id, userToken: token });
|
|
219
219
|
}
|
|
220
220
|
if (existingUser.status === "pending") {
|
|
221
|
+
this.notifyAdmins("user_join_request", "user", username, "", { dedup: true });
|
|
221
222
|
return this.json(res, 200, { status: "pending", userId: existingUser.id });
|
|
222
223
|
}
|
|
223
224
|
if (existingUser.status === "rejected") {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
225
|
+
if (body.reapply === true) {
|
|
226
|
+
this.userManager.resetToPending(existingUser.id);
|
|
227
|
+
this.notifyAdmins("user_join_request", "user", username, "");
|
|
228
|
+
this.opts.log.info(`Hub: rejected user "${username}" (${existingUser.id}) re-applied, reset to pending`);
|
|
229
|
+
return this.json(res, 200, { status: "pending", userId: existingUser.id });
|
|
230
|
+
}
|
|
231
|
+
return this.json(res, 200, { status: "rejected", userId: existingUser.id });
|
|
227
232
|
}
|
|
228
233
|
}
|
|
229
234
|
const user = this.userManager.createPendingUser({
|
|
@@ -802,10 +807,13 @@ export class HubServer {
|
|
|
802
807
|
return this.json(res, 404, { error: "not_found" });
|
|
803
808
|
}
|
|
804
809
|
|
|
805
|
-
private notifyAdmins(type: string, resource: string, title: string, fromUserId: string): void {
|
|
810
|
+
private notifyAdmins(type: string, resource: string, title: string, fromUserId: string, opts?: { dedup?: boolean; deduoWindowMs?: number }): void {
|
|
806
811
|
try {
|
|
807
812
|
const admins = this.opts.store.listHubUsers("active").filter(u => u.role === "admin" && u.id !== fromUserId);
|
|
808
813
|
for (const admin of admins) {
|
|
814
|
+
if (opts?.dedup && this.opts.store.hasRecentHubNotification(admin.id, type, resource, opts.deduoWindowMs ?? 300_000)) {
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
809
817
|
this.opts.store.insertHubNotification({ id: randomUUID(), userId: admin.id, type, resource, title });
|
|
810
818
|
}
|
|
811
819
|
} catch { /* best-effort */ }
|
package/src/recall/engine.ts
CHANGED
|
@@ -74,10 +74,49 @@ export class RecallEngine {
|
|
|
74
74
|
score: 1 / (i + 1),
|
|
75
75
|
}));
|
|
76
76
|
|
|
77
|
+
// Step 1c: Hub memories search (when sharing is enabled and hub_memories exist)
|
|
78
|
+
let hubMemFtsRanked: Array<{ id: string; score: number }> = [];
|
|
79
|
+
let hubMemVecRanked: Array<{ id: string; score: number }> = [];
|
|
80
|
+
if (query && this.ctx.config.sharing?.enabled) {
|
|
81
|
+
try {
|
|
82
|
+
const hubFtsHits = this.store.searchHubMemories(query, { maxResults: candidatePool });
|
|
83
|
+
hubMemFtsRanked = hubFtsHits.map(({ hit }, i) => ({
|
|
84
|
+
id: `hubmem:${hit.id}`, score: 1 / (i + 1),
|
|
85
|
+
}));
|
|
86
|
+
} catch { /* hub_memories table may not exist */ }
|
|
87
|
+
try {
|
|
88
|
+
const hubMemEmbs = this.store.getVisibleHubMemoryEmbeddings("");
|
|
89
|
+
if (hubMemEmbs.length > 0) {
|
|
90
|
+
const qv = await this.embedder.embedQuery(query).catch(() => null);
|
|
91
|
+
if (qv) {
|
|
92
|
+
const scored: Array<{ id: string; score: number }> = [];
|
|
93
|
+
for (const e of hubMemEmbs) {
|
|
94
|
+
let dot = 0, nA = 0, nB = 0;
|
|
95
|
+
for (let i = 0; i < qv.length && i < e.vector.length; i++) {
|
|
96
|
+
dot += qv[i] * e.vector[i]; nA += qv[i] * qv[i]; nB += e.vector[i] * e.vector[i];
|
|
97
|
+
}
|
|
98
|
+
const sim = nA > 0 && nB > 0 ? dot / (Math.sqrt(nA) * Math.sqrt(nB)) : 0;
|
|
99
|
+
if (sim > 0.3) {
|
|
100
|
+
scored.push({ id: `hubmem:${e.memoryId}`, score: sim });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
scored.sort((a, b) => b.score - a.score);
|
|
104
|
+
hubMemVecRanked = scored.slice(0, candidatePool);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} catch { /* best-effort */ }
|
|
108
|
+
if (hubMemFtsRanked.length > 0 || hubMemVecRanked.length > 0) {
|
|
109
|
+
this.ctx.log.debug(`recall: hub_memories candidates: fts=${hubMemFtsRanked.length}, vec=${hubMemVecRanked.length}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
77
113
|
// Step 2: RRF fusion
|
|
78
114
|
const ftsRanked = ftsCandidates.map((c) => ({ id: c.chunkId, score: c.score }));
|
|
79
115
|
const vecRanked = vecCandidates.map((c) => ({ id: c.chunkId, score: c.score }));
|
|
80
|
-
const
|
|
116
|
+
const allRankedLists = [ftsRanked, vecRanked, patternRanked];
|
|
117
|
+
if (hubMemFtsRanked.length > 0) allRankedLists.push(hubMemFtsRanked);
|
|
118
|
+
if (hubMemVecRanked.length > 0) allRankedLists.push(hubMemVecRanked);
|
|
119
|
+
const rrfScores = rrfFuse(allRankedLists, recallCfg.rrfK);
|
|
81
120
|
|
|
82
121
|
if (rrfScores.size === 0) {
|
|
83
122
|
this.recordQuery(query, maxResults, minScore, 0);
|
|
@@ -101,6 +140,11 @@ export class RecallEngine {
|
|
|
101
140
|
|
|
102
141
|
// Step 4: Time decay
|
|
103
142
|
const withTs = mmrResults.map((r) => {
|
|
143
|
+
if (r.id.startsWith("hubmem:")) {
|
|
144
|
+
const memId = r.id.slice(7);
|
|
145
|
+
const mem = this.store.getHubMemoryById(memId);
|
|
146
|
+
return { ...r, createdAt: mem?.createdAt ?? 0 };
|
|
147
|
+
}
|
|
104
148
|
const chunk = this.store.getChunk(r.id);
|
|
105
149
|
return { ...r, createdAt: chunk?.createdAt ?? 0 };
|
|
106
150
|
});
|
|
@@ -128,6 +172,34 @@ export class RecallEngine {
|
|
|
128
172
|
const hits: SearchHit[] = [];
|
|
129
173
|
for (const candidate of normalized) {
|
|
130
174
|
if (hits.length >= maxResults) break;
|
|
175
|
+
|
|
176
|
+
if (candidate.id.startsWith("hubmem:")) {
|
|
177
|
+
const memId = candidate.id.slice(7);
|
|
178
|
+
const mem = this.store.getHubMemoryById(memId);
|
|
179
|
+
if (!mem) continue;
|
|
180
|
+
if (roleFilter && mem.role !== roleFilter) continue;
|
|
181
|
+
hits.push({
|
|
182
|
+
summary: mem.summary || mem.content.slice(0, 200),
|
|
183
|
+
original_excerpt: mem.content,
|
|
184
|
+
ref: {
|
|
185
|
+
sessionKey: `hub-shared:${mem.sourceUserId}`,
|
|
186
|
+
chunkId: mem.id,
|
|
187
|
+
turnId: "",
|
|
188
|
+
seq: 0,
|
|
189
|
+
},
|
|
190
|
+
score: Math.round(candidate.score * 1000) / 1000,
|
|
191
|
+
taskId: null,
|
|
192
|
+
skillId: null,
|
|
193
|
+
owner: `hub-user:${mem.sourceUserId}`,
|
|
194
|
+
source: {
|
|
195
|
+
ts: mem.createdAt,
|
|
196
|
+
role: (mem.role || "assistant") as any,
|
|
197
|
+
sessionKey: `hub-shared:${mem.sourceUserId}`,
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
131
203
|
const chunk = this.store.getChunk(candidate.id);
|
|
132
204
|
if (!chunk) continue;
|
|
133
205
|
if (roleFilter && chunk.role !== roleFilter) continue;
|
package/src/storage/sqlite.ts
CHANGED
|
@@ -2156,6 +2156,14 @@ export class SqliteStore {
|
|
|
2156
2156
|
).run(n.id, n.userId, n.type, n.resource, n.title, n.message ?? '', Date.now());
|
|
2157
2157
|
}
|
|
2158
2158
|
|
|
2159
|
+
hasRecentHubNotification(userId: string, type: string, resource: string, windowMs: number = 300_000): boolean {
|
|
2160
|
+
const since = Date.now() - windowMs;
|
|
2161
|
+
const row = this.db.prepare(
|
|
2162
|
+
'SELECT COUNT(*) AS cnt FROM hub_notifications WHERE user_id = ? AND type = ? AND resource = ? AND created_at > ?'
|
|
2163
|
+
).get(userId, type, resource, since) as { cnt: number };
|
|
2164
|
+
return row.cnt > 0;
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2159
2167
|
listHubNotifications(userId: string, opts?: { unreadOnly?: boolean; limit?: number }): Array<{ id: string; userId: string; type: string; resource: string; title: string; message: string; read: boolean; createdAt: number }> {
|
|
2160
2168
|
const where = opts?.unreadOnly ? 'WHERE user_id = ? AND read = 0' : 'WHERE user_id = ?';
|
|
2161
2169
|
const limit = opts?.limit ?? 50;
|