@memtensor/memos-local-openclaw-plugin 1.0.5 → 1.0.6-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.
- package/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +24 -0
- package/dist/capture/index.js.map +1 -1
- package/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +33 -5
- package/dist/client/connector.js.map +1 -1
- package/dist/client/hub.d.ts.map +1 -1
- package/dist/client/hub.js +4 -0
- package/dist/client/hub.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 +116 -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 +32 -86
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/ingest/providers/openai.d.ts.map +1 -1
- package/dist/ingest/providers/openai.js +29 -13
- package/dist/ingest/providers/openai.js.map +1 -1
- package/dist/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +33 -32
- package/dist/recall/engine.js.map +1 -1
- package/dist/storage/sqlite.d.ts +43 -7
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +179 -58
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/tools/memory-get.d.ts.map +1 -1
- package/dist/tools/memory-get.js +4 -1
- package/dist/tools/memory-get.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.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 +25 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +503 -206
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +273 -282
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/scripts/native-binding.cjs +32 -0
- package/scripts/postinstall.cjs +24 -11
- package/src/capture/index.ts +36 -0
- package/src/client/connector.ts +32 -5
- package/src/client/hub.ts +4 -0
- package/src/hub/server.ts +110 -50
- package/src/ingest/providers/index.ts +37 -92
- package/src/ingest/providers/openai.ts +31 -13
- package/src/recall/engine.ts +32 -30
- package/src/storage/sqlite.ts +196 -63
- package/src/tools/memory-get.ts +4 -1
- package/src/types.ts +2 -0
- package/src/update-check.ts +2 -7
- package/src/viewer/html.ts +115 -27
- package/src/viewer/server.ts +483 -172
- package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
- package/prebuilds/darwin-x64/better_sqlite3.node +0 -0
- package/prebuilds/linux-x64/better_sqlite3.node +0 -0
- package/prebuilds/win32-x64/better_sqlite3.node +0 -0
- package/telemetry.credentials.json +0 -5
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": "0.
|
|
6
|
+
"version": "1.0.6-beta.10",
|
|
7
7
|
"skills": [
|
|
8
8
|
"skill/memos-memory-guide"
|
|
9
9
|
],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memtensor/memos-local-openclaw-plugin",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6-beta.10",
|
|
4
4
|
"description": "MemOS Local memory plugin for OpenClaw — full-write, hybrid-recall, progressive retrieval",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"dist",
|
|
12
12
|
"skill",
|
|
13
13
|
"prebuilds",
|
|
14
|
+
"scripts/native-binding.cjs",
|
|
14
15
|
"scripts/postinstall.cjs",
|
|
15
16
|
"openclaw.plugin.json",
|
|
16
17
|
"telemetry.credentials.json",
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
function errorMessage(error) {
|
|
4
|
+
if (error && typeof error.message === "string") return error.message;
|
|
5
|
+
return String(error || "Unknown native binding error");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function defaultLoadBinding(bindingPath) {
|
|
9
|
+
process.dlopen({ exports: {} }, bindingPath);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function validateNativeBinding(bindingPath, loadBinding = defaultLoadBinding) {
|
|
13
|
+
if (!bindingPath) {
|
|
14
|
+
return { ok: false, reason: "missing", message: "Native binding path not found" };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
loadBinding(bindingPath);
|
|
19
|
+
return { ok: true, reason: "ok", message: "" };
|
|
20
|
+
} catch (error) {
|
|
21
|
+
const message = errorMessage(error);
|
|
22
|
+
if (/NODE_MODULE_VERSION/.test(message)) {
|
|
23
|
+
return { ok: false, reason: "node-module-version", message };
|
|
24
|
+
}
|
|
25
|
+
return { ok: false, reason: "load-error", message };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = {
|
|
30
|
+
defaultLoadBinding,
|
|
31
|
+
validateNativeBinding,
|
|
32
|
+
};
|
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";
|
|
@@ -23,6 +24,11 @@ function phase(n, title) {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
const pluginDir = path.resolve(__dirname, "..");
|
|
27
|
+
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
28
|
+
|
|
29
|
+
function normalizePathForMatch(p) {
|
|
30
|
+
return path.resolve(p).replace(/^\\\\\?\\/, "").replace(/\\/g, "/").toLowerCase();
|
|
31
|
+
}
|
|
26
32
|
|
|
27
33
|
console.log(`
|
|
28
34
|
${CYAN}${BOLD}┌──────────────────────────────────────────────────┐
|
|
@@ -42,7 +48,8 @@ log(`Node: ${process.version} Platform: ${process.platform}-${process.arch}`);
|
|
|
42
48
|
* ═══════════════════════════════════════════════════════════ */
|
|
43
49
|
|
|
44
50
|
function cleanStaleArtifacts() {
|
|
45
|
-
const
|
|
51
|
+
const pluginDirNorm = normalizePathForMatch(pluginDir);
|
|
52
|
+
const isExtensionsDir = pluginDirNorm.includes("/.openclaw/extensions/");
|
|
46
53
|
if (!isExtensionsDir) return;
|
|
47
54
|
|
|
48
55
|
const pkgPath = path.join(pluginDir, "package.json");
|
|
@@ -133,10 +140,10 @@ function ensureDependencies() {
|
|
|
133
140
|
log("Running: npm install --omit=dev ...");
|
|
134
141
|
|
|
135
142
|
const startMs = Date.now();
|
|
136
|
-
const result = spawnSync(
|
|
143
|
+
const result = spawnSync(npmCmd, ["install", "--omit=dev"], {
|
|
137
144
|
cwd: pluginDir,
|
|
138
145
|
stdio: "pipe",
|
|
139
|
-
shell:
|
|
146
|
+
shell: false,
|
|
140
147
|
timeout: 120_000,
|
|
141
148
|
});
|
|
142
149
|
const elapsed = ((Date.now() - startMs) / 1000).toFixed(1);
|
|
@@ -223,8 +230,8 @@ function cleanupLegacy() {
|
|
|
223
230
|
newEntry.source = oldSource
|
|
224
231
|
.replace(/memos-lite-openclaw-plugin/g, "memos-local-openclaw-plugin")
|
|
225
232
|
.replace(/memos-lite/g, "memos-local-openclaw-plugin")
|
|
226
|
-
.replace(
|
|
227
|
-
.replace(
|
|
233
|
+
.replace(/[\\/]memos-local[\\/]/g, `${path.sep}memos-local-openclaw-plugin${path.sep}`)
|
|
234
|
+
.replace(/[\\/]memos-local$/g, `${path.sep}memos-local-openclaw-plugin`);
|
|
228
235
|
if (newEntry.source !== oldSource) {
|
|
229
236
|
log(`Updated source path: ${DIM}${oldSource}${RESET} → ${GREEN}${newEntry.source}${RESET}`);
|
|
230
237
|
cfgChanged = true;
|
|
@@ -371,25 +378,31 @@ function findSqliteBinding() {
|
|
|
371
378
|
|
|
372
379
|
function sqliteBindingsExist() {
|
|
373
380
|
const found = findSqliteBinding();
|
|
374
|
-
if (found)
|
|
375
|
-
|
|
376
|
-
|
|
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.");
|
|
377
389
|
}
|
|
390
|
+
warn(`${DIM}${status.message}${RESET}`);
|
|
378
391
|
return false;
|
|
379
392
|
}
|
|
380
393
|
|
|
381
394
|
if (sqliteBindingsExist()) {
|
|
382
395
|
ok("better-sqlite3 is ready.");
|
|
383
396
|
} else {
|
|
384
|
-
warn("better-sqlite3 native bindings
|
|
397
|
+
warn("better-sqlite3 native bindings are missing or not loadable.");
|
|
385
398
|
log(`Searched in: ${DIM}${sqliteModulePath}/build/${RESET}`);
|
|
386
399
|
log("Running: npm rebuild better-sqlite3 (may take 30-60s)...");
|
|
387
400
|
|
|
388
401
|
const startMs = Date.now();
|
|
389
|
-
const result = spawnSync(
|
|
402
|
+
const result = spawnSync(npmCmd, ["rebuild", "better-sqlite3"], {
|
|
390
403
|
cwd: pluginDir,
|
|
391
404
|
stdio: "pipe",
|
|
392
|
-
shell:
|
|
405
|
+
shell: false,
|
|
393
406
|
timeout: 180_000,
|
|
394
407
|
});
|
|
395
408
|
const elapsed = ((Date.now() - startMs) / 1000).toFixed(1);
|
package/src/capture/index.ts
CHANGED
|
@@ -4,6 +4,17 @@ const SKIP_ROLES: Set<Role> = new Set(["system"]);
|
|
|
4
4
|
|
|
5
5
|
const SYSTEM_BOILERPLATE_RE = /^A new session was started via \/new or \/reset\b/;
|
|
6
6
|
|
|
7
|
+
// Boot-check / memory-system injection patterns that should never be stored.
|
|
8
|
+
const BOOT_CHECK_RE = /^(?:You are running a boot check|Read HEARTBEAT\.md if it exists|## Memory system — ACTION REQUIRED)/;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Returns true for sentinel reply values that carry no user-facing content.
|
|
12
|
+
*/
|
|
13
|
+
function isSentinelReply(text: string): boolean {
|
|
14
|
+
const t = text.trim();
|
|
15
|
+
return t === "NO_REPLY" || t === "HEARTBEAT_OK" || t === "HEARTBEAT_CHECK";
|
|
16
|
+
}
|
|
17
|
+
|
|
7
18
|
const SELF_TOOLS = new Set([
|
|
8
19
|
"memory_search",
|
|
9
20
|
"memory_timeline",
|
|
@@ -61,6 +72,16 @@ export function captureMessages(
|
|
|
61
72
|
if (SKIP_ROLES.has(role)) continue;
|
|
62
73
|
if (!msg.content || msg.content.trim().length === 0) continue;
|
|
63
74
|
|
|
75
|
+
// Skip sentinel replies and boot-check prompts for ALL roles.
|
|
76
|
+
if (isSentinelReply(msg.content)) {
|
|
77
|
+
log.debug(`Skipping sentinel reply`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (BOOT_CHECK_RE.test(msg.content.trim())) {
|
|
81
|
+
log.debug(`Skipping boot-check injection: ${msg.content.slice(0, 60)}...`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
64
85
|
if (role === "tool" && msg.toolName && SELF_TOOLS.has(msg.toolName)) {
|
|
65
86
|
log.debug(`Skipping self-tool result: ${msg.toolName}`);
|
|
66
87
|
continue;
|
|
@@ -232,6 +253,21 @@ function stripMemoryInjection(text: string): string {
|
|
|
232
253
|
"",
|
|
233
254
|
).trim();
|
|
234
255
|
|
|
256
|
+
// ## Memory system — ACTION REQUIRED\n...
|
|
257
|
+
cleaned = cleaned.replace(
|
|
258
|
+
/## Memory system — ACTION REQUIRED[\s\S]*?(?=\n\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+\d{4}-\d{2}-\d{2}|\n\[Subagent)/,
|
|
259
|
+
"",
|
|
260
|
+
).trim();
|
|
261
|
+
|
|
262
|
+
// You are running a boot check. Follow BOOT.md instructions exactly.\n...
|
|
263
|
+
cleaned = cleaned.replace(
|
|
264
|
+
/^You are running a boot check[\s\S]*?(?=\n\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+\d{4}-\d{2}-\d{2}|\n\[Subagent)/m,
|
|
265
|
+
"",
|
|
266
|
+
).trim();
|
|
267
|
+
|
|
268
|
+
// Standalone NO_REPLY / HEARTBEAT_OK that leaked into user messages
|
|
269
|
+
cleaned = cleaned.replace(/^\s*(?:NO_REPLY|HEARTBEAT_OK|HEARTBEAT_CHECK)\s*$/gm, "").trim();
|
|
270
|
+
|
|
235
271
|
// Old format: ## Retrieved memories from past conversations\n\nCRITICAL INSTRUCTION:...
|
|
236
272
|
const recallIdx = cleaned.indexOf("## Retrieved memories from past conversations");
|
|
237
273
|
if (recallIdx !== -1) {
|
package/src/client/connector.ts
CHANGED
|
@@ -49,6 +49,13 @@ export async function connectToHub(store: SqliteStore, config: MemosLocalConfig,
|
|
|
49
49
|
}) as any;
|
|
50
50
|
if (result.status === "active" && result.userToken) {
|
|
51
51
|
log.info(`Pending user approved! Connecting with token. userId=${persisted.userId}`);
|
|
52
|
+
let approvedHubInstanceId = persisted.hubInstanceId || "";
|
|
53
|
+
if (!approvedHubInstanceId) {
|
|
54
|
+
try {
|
|
55
|
+
const info = await hubRequestJson(hubUrl, "", "/api/v1/hub/info", { method: "GET" }) as any;
|
|
56
|
+
approvedHubInstanceId = String(info?.hubInstanceId ?? "");
|
|
57
|
+
} catch { /* best-effort */ }
|
|
58
|
+
}
|
|
52
59
|
store.setClientHubConnection({
|
|
53
60
|
hubUrl,
|
|
54
61
|
userId: persisted.userId,
|
|
@@ -58,6 +65,7 @@ export async function connectToHub(store: SqliteStore, config: MemosLocalConfig,
|
|
|
58
65
|
connectedAt: Date.now(),
|
|
59
66
|
identityKey: persisted.identityKey || "",
|
|
60
67
|
lastKnownStatus: "active",
|
|
68
|
+
hubInstanceId: approvedHubInstanceId,
|
|
61
69
|
});
|
|
62
70
|
return store.getClientHubConnection()!;
|
|
63
71
|
}
|
|
@@ -87,7 +95,10 @@ export async function connectToHub(store: SqliteStore, config: MemosLocalConfig,
|
|
|
87
95
|
}
|
|
88
96
|
|
|
89
97
|
const hubUrl = normalizeHubUrl(hubAddress);
|
|
90
|
-
const me = await
|
|
98
|
+
const [me, info] = await Promise.all([
|
|
99
|
+
hubRequestJson(hubUrl, userToken, "/api/v1/hub/me", { method: "GET" }),
|
|
100
|
+
hubRequestJson(hubUrl, "", "/api/v1/hub/info", { method: "GET" }).catch(() => null),
|
|
101
|
+
]) as [any, any];
|
|
91
102
|
const persisted = store.getClientHubConnection();
|
|
92
103
|
store.setClientHubConnection({
|
|
93
104
|
hubUrl,
|
|
@@ -98,6 +109,7 @@ export async function connectToHub(store: SqliteStore, config: MemosLocalConfig,
|
|
|
98
109
|
connectedAt: Date.now(),
|
|
99
110
|
identityKey: persisted?.identityKey || String(me.identityKey ?? ""),
|
|
100
111
|
lastKnownStatus: "active",
|
|
112
|
+
hubInstanceId: String(info?.hubInstanceId ?? persisted?.hubInstanceId ?? ""),
|
|
101
113
|
});
|
|
102
114
|
return store.getClientHubConnection()!;
|
|
103
115
|
}
|
|
@@ -148,6 +160,7 @@ export async function getHubStatus(store: SqliteStore, config: MemosLocalConfig)
|
|
|
148
160
|
connectedAt: Date.now(),
|
|
149
161
|
identityKey: conn.identityKey || "",
|
|
150
162
|
lastKnownStatus: "active",
|
|
163
|
+
hubInstanceId: conn.hubInstanceId || "",
|
|
151
164
|
});
|
|
152
165
|
const me = await hubRequestJson(normalizeHubUrl(hubAddress), result.userToken, "/api/v1/hub/me", { method: "GET" }) as any;
|
|
153
166
|
return {
|
|
@@ -216,22 +229,28 @@ export async function getHubStatus(store: SqliteStore, config: MemosLocalConfig)
|
|
|
216
229
|
body: JSON.stringify({ teamToken, userId: conn.userId }),
|
|
217
230
|
}) as any;
|
|
218
231
|
if (regResult.status === "active" && regResult.userToken) {
|
|
219
|
-
|
|
232
|
+
const updatedConn = {
|
|
220
233
|
...conn,
|
|
221
234
|
hubUrl: normalizeHubUrl(hubAddress),
|
|
222
235
|
userToken: regResult.userToken,
|
|
223
236
|
connectedAt: Date.now(),
|
|
224
237
|
lastKnownStatus: "active",
|
|
225
|
-
}
|
|
238
|
+
};
|
|
239
|
+
store.setClientHubConnection(updatedConn);
|
|
226
240
|
try {
|
|
227
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
|
+
}
|
|
228
247
|
return {
|
|
229
248
|
connected: true,
|
|
230
249
|
hubUrl: normalizeHubUrl(hubAddress),
|
|
231
250
|
user: {
|
|
232
251
|
id: String(me.id),
|
|
233
|
-
username:
|
|
234
|
-
role:
|
|
252
|
+
username: latestUsername,
|
|
253
|
+
role: latestRole,
|
|
235
254
|
status: String(me.status ?? "active"),
|
|
236
255
|
groups: Array.isArray(me.groups) ? me.groups : [],
|
|
237
256
|
},
|
|
@@ -293,6 +312,12 @@ export async function autoJoinHub(
|
|
|
293
312
|
const existingIdentityKey = persisted?.identityKey || "";
|
|
294
313
|
|
|
295
314
|
log.info(`Joining Hub at ${hubUrl} as "${username}"...`);
|
|
315
|
+
let hubInstanceId = "";
|
|
316
|
+
try {
|
|
317
|
+
const info = await hubRequestJson(hubUrl, "", "/api/v1/hub/info", { method: "GET" }) as any;
|
|
318
|
+
hubInstanceId = String(info?.hubInstanceId ?? "");
|
|
319
|
+
} catch { /* best-effort */ }
|
|
320
|
+
|
|
296
321
|
const result = await hubRequestJson(hubUrl, "", "/api/v1/hub/join", {
|
|
297
322
|
method: "POST",
|
|
298
323
|
body: JSON.stringify({ teamToken, username, deviceName: hostname, clientIp, identityKey: existingIdentityKey }),
|
|
@@ -311,6 +336,7 @@ export async function autoJoinHub(
|
|
|
311
336
|
connectedAt: Date.now(),
|
|
312
337
|
identityKey: returnedIdentityKey,
|
|
313
338
|
lastKnownStatus: "pending",
|
|
339
|
+
hubInstanceId,
|
|
314
340
|
});
|
|
315
341
|
throw new PendingApprovalError(result.userId);
|
|
316
342
|
}
|
|
@@ -337,6 +363,7 @@ export async function autoJoinHub(
|
|
|
337
363
|
connectedAt: Date.now(),
|
|
338
364
|
identityKey: returnedIdentityKey,
|
|
339
365
|
lastKnownStatus: "active",
|
|
366
|
+
hubInstanceId,
|
|
340
367
|
});
|
|
341
368
|
return store.getClientHubConnection()!;
|
|
342
369
|
}
|
package/src/client/hub.ts
CHANGED
|
@@ -140,6 +140,7 @@ export async function hubUpdateUsername(
|
|
|
140
140
|
newUsername: string,
|
|
141
141
|
): Promise<{ ok: boolean; username: string; userToken: string }> {
|
|
142
142
|
const client = await resolveHubClient(store, ctx);
|
|
143
|
+
const persisted = store.getClientHubConnection();
|
|
143
144
|
const result = await hubRequestJson(client.hubUrl, client.userToken, "/api/v1/hub/me/update-profile", {
|
|
144
145
|
method: "POST",
|
|
145
146
|
body: JSON.stringify({ username: newUsername }),
|
|
@@ -152,6 +153,9 @@ export async function hubUpdateUsername(
|
|
|
152
153
|
userToken: result.userToken,
|
|
153
154
|
role: client.role as "admin" | "member",
|
|
154
155
|
connectedAt: Date.now(),
|
|
156
|
+
identityKey: persisted?.identityKey || "",
|
|
157
|
+
lastKnownStatus: "active",
|
|
158
|
+
hubInstanceId: persisted?.hubInstanceId || "",
|
|
155
159
|
});
|
|
156
160
|
}
|
|
157
161
|
return result;
|
package/src/hub/server.ts
CHANGED
|
@@ -21,6 +21,7 @@ type HubAuthState = {
|
|
|
21
21
|
authSecret: string;
|
|
22
22
|
bootstrapAdminUserId?: string;
|
|
23
23
|
bootstrapAdminToken?: string;
|
|
24
|
+
hubInstanceId?: string;
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
export class HubServer {
|
|
@@ -123,6 +124,8 @@ export class HubServer {
|
|
|
123
124
|
this.initOnlineTracking();
|
|
124
125
|
this.offlineCheckTimer = setInterval(() => this.checkOfflineUsers(), HubServer.OFFLINE_CHECK_INTERVAL_MS);
|
|
125
126
|
|
|
127
|
+
this.backfillMemoryEmbeddings();
|
|
128
|
+
|
|
126
129
|
return `http://127.0.0.1:${hubPort}`;
|
|
127
130
|
}
|
|
128
131
|
|
|
@@ -168,13 +171,23 @@ export class HubServer {
|
|
|
168
171
|
return this.authState.authSecret;
|
|
169
172
|
}
|
|
170
173
|
|
|
174
|
+
get hubInstanceId(): string {
|
|
175
|
+
return this.authState.hubInstanceId ?? "";
|
|
176
|
+
}
|
|
177
|
+
|
|
171
178
|
private loadAuthState(): HubAuthState {
|
|
172
179
|
try {
|
|
173
180
|
const raw = fs.readFileSync(this.authStatePath, "utf8");
|
|
174
181
|
const parsed = JSON.parse(raw) as HubAuthState;
|
|
175
|
-
if (parsed.authSecret)
|
|
182
|
+
if (parsed.authSecret) {
|
|
183
|
+
if (!parsed.hubInstanceId) {
|
|
184
|
+
parsed.hubInstanceId = randomUUID();
|
|
185
|
+
fs.writeFileSync(this.authStatePath, JSON.stringify(parsed, null, 2), "utf8");
|
|
186
|
+
}
|
|
187
|
+
return parsed;
|
|
188
|
+
}
|
|
176
189
|
} catch {}
|
|
177
|
-
const initial = { authSecret: randomBytes(32).toString("hex")
|
|
190
|
+
const initial: HubAuthState = { authSecret: randomBytes(32).toString("hex"), hubInstanceId: randomUUID() };
|
|
178
191
|
fs.mkdirSync(path.dirname(this.authStatePath), { recursive: true });
|
|
179
192
|
fs.writeFileSync(this.authStatePath, JSON.stringify(initial, null, 2), "utf8");
|
|
180
193
|
return initial;
|
|
@@ -215,10 +228,38 @@ export class HubServer {
|
|
|
215
228
|
});
|
|
216
229
|
}
|
|
217
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
|
+
|
|
218
258
|
private embedMemoryAsync(memoryId: string, summary: string, content: string): void {
|
|
219
259
|
const embedder = this.opts.embedder;
|
|
220
260
|
if (!embedder) return;
|
|
221
|
-
const text = summary || content.slice(0, 500);
|
|
261
|
+
const text = (summary || content || "").slice(0, 500);
|
|
262
|
+
if (!text) return;
|
|
222
263
|
embedder.embed([text]).then((vectors) => {
|
|
223
264
|
if (vectors[0]) {
|
|
224
265
|
this.opts.store.upsertHubMemoryEmbedding(memoryId, new Float32Array(vectors[0]));
|
|
@@ -238,6 +279,7 @@ export class HubServer {
|
|
|
238
279
|
teamName: this.teamName,
|
|
239
280
|
version: "0.0.0",
|
|
240
281
|
apiVersion: "v1",
|
|
282
|
+
hubInstanceId: this.hubInstanceId,
|
|
241
283
|
});
|
|
242
284
|
}
|
|
243
285
|
|
|
@@ -252,59 +294,64 @@ export class HubServer {
|
|
|
252
294
|
|| (req.headers["x-forwarded-for"] as string)?.split(",")[0]?.trim()
|
|
253
295
|
|| req.socket.remoteAddress || "";
|
|
254
296
|
const identityKey = typeof body.identityKey === "string" ? body.identityKey.trim() : "";
|
|
297
|
+
const dryRun = body.dryRun === true;
|
|
255
298
|
|
|
256
|
-
|
|
299
|
+
const identityMatch = identityKey
|
|
257
300
|
? this.userManager.findByIdentityKey(identityKey)
|
|
258
301
|
: null;
|
|
259
|
-
if (!existingUser) {
|
|
260
|
-
const existingUsers = this.opts.store.listHubUsers();
|
|
261
|
-
existingUser = existingUsers.find(u => u.username === username && u.status !== "left" && u.status !== "removed") ?? null;
|
|
262
|
-
}
|
|
263
302
|
|
|
264
|
-
if (
|
|
265
|
-
|
|
303
|
+
if (identityMatch) {
|
|
304
|
+
if (!dryRun) {
|
|
305
|
+
try { this.opts.store.updateHubUserActivity(identityMatch.id, joinIp); } catch { /* best-effort */ }
|
|
306
|
+
}
|
|
266
307
|
|
|
267
|
-
if (
|
|
308
|
+
if (identityMatch.status === "active") {
|
|
309
|
+
if (dryRun) return this.json(res, 200, { status: "active", dryRun: true });
|
|
268
310
|
const token = issueUserToken(
|
|
269
|
-
{ userId:
|
|
311
|
+
{ userId: identityMatch.id, username: identityMatch.username, role: identityMatch.role, status: "active" },
|
|
270
312
|
this.authSecret,
|
|
271
313
|
);
|
|
272
|
-
this.userManager.approveUser(
|
|
273
|
-
|
|
274
|
-
this.opts.store.upsertHubUser({ ...existingUser, identityKey });
|
|
275
|
-
}
|
|
276
|
-
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 });
|
|
277
316
|
}
|
|
278
|
-
if (
|
|
279
|
-
this.
|
|
280
|
-
|
|
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 });
|
|
281
321
|
}
|
|
282
|
-
if (
|
|
322
|
+
if (identityMatch.status === "rejected") {
|
|
323
|
+
if (dryRun) return this.json(res, 200, { status: "rejected", dryRun: true });
|
|
283
324
|
if (body.reapply === true) {
|
|
284
|
-
this.userManager.resetToPending(
|
|
285
|
-
this.notifyAdmins("user_join_request", "user", username, "");
|
|
286
|
-
this.opts.log.info(`Hub: rejected user "${username}" (${
|
|
287
|
-
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 });
|
|
288
329
|
}
|
|
289
|
-
return this.json(res, 200, { status: "rejected", userId:
|
|
290
|
-
}
|
|
291
|
-
if (existingUser.status === "removed") {
|
|
292
|
-
this.userManager.rejoinUser(existingUser.id);
|
|
293
|
-
this.notifyAdmins("user_join_request", "user", username, "", { dedup: true });
|
|
294
|
-
this.opts.log.info(`Hub: removed user "${username}" (${existingUser.id}) re-applied via rejoin, reset to pending`);
|
|
295
|
-
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 });
|
|
296
331
|
}
|
|
297
|
-
if (
|
|
298
|
-
this.
|
|
299
|
-
this.
|
|
300
|
-
this.
|
|
301
|
-
|
|
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 });
|
|
302
338
|
}
|
|
303
|
-
if (
|
|
304
|
-
return this.json(res, 200, { status: "blocked", userId:
|
|
339
|
+
if (identityMatch.status === "blocked") {
|
|
340
|
+
return this.json(res, 200, { status: "blocked", userId: identityMatch.id });
|
|
305
341
|
}
|
|
306
342
|
}
|
|
307
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
|
+
|
|
308
355
|
const generatedIdentityKey = identityKey || randomUUID();
|
|
309
356
|
const user = this.userManager.createPendingUser({
|
|
310
357
|
username,
|
|
@@ -382,10 +429,13 @@ export class HubServer {
|
|
|
382
429
|
}
|
|
383
430
|
|
|
384
431
|
if (req.method === "POST" && routePath === "/api/v1/hub/leave") {
|
|
432
|
+
this.opts.store.deleteHubMemoriesByUser(auth.userId);
|
|
433
|
+
this.opts.store.deleteHubTasksByUser(auth.userId);
|
|
434
|
+
this.opts.store.deleteHubSkillsByUser(auth.userId);
|
|
385
435
|
this.userManager.markUserLeft(auth.userId);
|
|
386
436
|
this.knownOnlineUsers.delete(auth.userId);
|
|
387
437
|
this.notifyAdmins("user_left", "user", auth.username, auth.userId);
|
|
388
|
-
this.opts.log.info(`Hub: user "${auth.username}" (${auth.userId}) left voluntarily, status set to "left"`);
|
|
438
|
+
this.opts.log.info(`Hub: user "${auth.username}" (${auth.userId}) left voluntarily, resources cleaned, status set to "left"`);
|
|
389
439
|
return this.json(res, 200, { ok: true });
|
|
390
440
|
}
|
|
391
441
|
|
|
@@ -530,6 +580,12 @@ export class HubServer {
|
|
|
530
580
|
this.authState.bootstrapAdminToken = newToken;
|
|
531
581
|
this.saveAuthState();
|
|
532
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 */ }
|
|
533
589
|
this.opts.log.info(`Hub: admin "${auth.userId}" renamed user "${userId}" to "${newUsername}"`);
|
|
534
590
|
return this.json(res, 200, { ok: true, username: newUsername });
|
|
535
591
|
}
|
|
@@ -611,9 +667,7 @@ export class HubServer {
|
|
|
611
667
|
createdAt: existing?.createdAt ?? now,
|
|
612
668
|
updatedAt: now,
|
|
613
669
|
});
|
|
614
|
-
|
|
615
|
-
this.embedMemoryAsync(memoryId, String(m.summary || ""), String(m.content || ""));
|
|
616
|
-
}
|
|
670
|
+
this.embedMemoryAsync(memoryId, String(m.summary || ""), String(m.content || ""));
|
|
617
671
|
if (!existing) {
|
|
618
672
|
this.notifyAdmins("resource_shared", "memory", String(m.summary || m.content?.slice(0, 60) || memoryId), auth.userId);
|
|
619
673
|
}
|
|
@@ -660,24 +714,30 @@ export class HubServer {
|
|
|
660
714
|
// Track which IDs are memories vs chunks
|
|
661
715
|
const memoryIdSet = new Set(memFtsHits.map(({ hit }) => hit.id));
|
|
662
716
|
|
|
663
|
-
//
|
|
717
|
+
// Two-stage retrieval: FTS candidates first, then embed + cosine rerank
|
|
664
718
|
let mergedIds: string[];
|
|
665
719
|
if (this.opts.embedder) {
|
|
666
720
|
try {
|
|
667
721
|
const [queryVec] = await this.opts.embedder.embed([query]);
|
|
668
722
|
if (queryVec) {
|
|
669
723
|
const allEmb = this.opts.store.getVisibleHubEmbeddings(auth.userId);
|
|
670
|
-
const memEmb = this.opts.store.getVisibleHubMemoryEmbeddings(auth.userId);
|
|
671
724
|
const scored: Array<{ id: string; score: number }> = [];
|
|
672
|
-
const cosineSim = (
|
|
725
|
+
const cosineSim = (a: Float32Array | number[], b: number[]) => {
|
|
673
726
|
let dot = 0, nA = 0, nB = 0;
|
|
674
|
-
|
|
675
|
-
|
|
727
|
+
const len = Math.min(a.length, b.length);
|
|
728
|
+
for (let i = 0; i < len; i++) {
|
|
729
|
+
dot += a[i] * b[i]; nA += a[i] * a[i]; nB += b[i] * b[i];
|
|
676
730
|
}
|
|
677
731
|
return nA > 0 && nB > 0 ? dot / (Math.sqrt(nA) * Math.sqrt(nB)) : 0;
|
|
678
732
|
};
|
|
679
|
-
for (const e of allEmb) scored.push({ id: e.chunkId, score: cosineSim(e.vector) });
|
|
680
|
-
|
|
733
|
+
for (const e of allEmb) scored.push({ id: e.chunkId, score: cosineSim(e.vector, queryVec) });
|
|
734
|
+
|
|
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);
|
|
739
|
+
}
|
|
740
|
+
|
|
681
741
|
scored.sort((a, b) => b.score - a.score);
|
|
682
742
|
const topScored = scored.slice(0, maxResults * 2);
|
|
683
743
|
|