@memtensor/memos-local-openclaw-plugin 1.0.5 → 1.0.6-beta.1
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 +23 -1
- 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 +1 -1
- package/dist/hub/server.d.ts.map +1 -1
- package/dist/hub/server.js +39 -31
- package/dist/hub/server.js.map +1 -1
- package/dist/ingest/providers/index.d.ts.map +1 -1
- package/dist/ingest/providers/index.js +16 -86
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/ingest/providers/openai.d.ts +3 -0
- package/dist/ingest/providers/openai.d.ts.map +1 -1
- package/dist/ingest/providers/openai.js +34 -19
- package/dist/ingest/providers/openai.js.map +1 -1
- package/dist/recall/engine.d.ts.map +1 -1
- package/dist/recall/engine.js +28 -19
- package/dist/recall/engine.js.map +1 -1
- package/dist/storage/sqlite.d.ts +30 -7
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +139 -60
- 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/viewer/server.d.ts +24 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +332 -130
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +65 -29
- package/package.json +1 -1
- package/scripts/postinstall.cjs +21 -5
- package/src/capture/index.ts +36 -0
- package/src/client/connector.ts +22 -1
- package/src/client/hub.ts +4 -0
- package/src/hub/server.ts +42 -26
- package/src/ingest/providers/index.ts +30 -93
- package/src/ingest/providers/openai.ts +32 -15
- package/src/recall/engine.ts +28 -19
- package/src/storage/sqlite.ts +156 -65
- package/src/tools/memory-get.ts +4 -1
- package/src/types.ts +2 -0
- package/src/viewer/server.ts +313 -125
- 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/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
|
9
9
|
import { Type } from "@sinclair/typebox";
|
|
10
10
|
import * as fs from "fs";
|
|
11
11
|
import * as path from "path";
|
|
12
|
+
import { createRequire } from "node:module";
|
|
12
13
|
import { fileURLToPath } from "url";
|
|
13
14
|
import { buildContext } from "./src/config";
|
|
14
15
|
import type { HostModelsConfig } from "./src/openclaw-api";
|
|
@@ -83,25 +84,56 @@ const memosLocalPlugin = {
|
|
|
83
84
|
configSchema: pluginConfigSchema,
|
|
84
85
|
|
|
85
86
|
register(api: OpenClawPluginApi) {
|
|
86
|
-
|
|
87
|
-
const
|
|
87
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
88
|
+
const localRequire = createRequire(import.meta.url);
|
|
89
|
+
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
90
|
+
|
|
91
|
+
function detectPluginDir(startDir: string): string {
|
|
92
|
+
let cur = startDir;
|
|
93
|
+
for (let i = 0; i < 6; i++) {
|
|
94
|
+
const pkg = path.join(cur, "package.json");
|
|
95
|
+
if (fs.existsSync(pkg)) return cur;
|
|
96
|
+
const parent = path.dirname(cur);
|
|
97
|
+
if (parent === cur) break;
|
|
98
|
+
cur = parent;
|
|
99
|
+
}
|
|
100
|
+
return startDir;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const pluginDir = detectPluginDir(moduleDir);
|
|
88
104
|
|
|
89
105
|
function normalizeFsPath(p: string): string {
|
|
90
|
-
return path.resolve(p).replace(
|
|
106
|
+
return path.resolve(p).replace(/^\\\\\?\\/, "").toLowerCase();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isPathInside(baseDir: string, targetPath: string): boolean {
|
|
110
|
+
const baseNorm = normalizeFsPath(baseDir);
|
|
111
|
+
const targetNorm = normalizeFsPath(targetPath);
|
|
112
|
+
const rel = path.relative(baseNorm, targetNorm);
|
|
113
|
+
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function runNpm(args: string[]) {
|
|
117
|
+
const { spawnSync } = localRequire("child_process") as typeof import("node:child_process");
|
|
118
|
+
return spawnSync(npmCmd, args, {
|
|
119
|
+
cwd: pluginDir,
|
|
120
|
+
stdio: "pipe",
|
|
121
|
+
shell: false,
|
|
122
|
+
timeout: 120_000,
|
|
123
|
+
});
|
|
91
124
|
}
|
|
92
125
|
|
|
93
126
|
let sqliteReady = false;
|
|
94
127
|
|
|
95
128
|
function trySqliteLoad(): boolean {
|
|
96
129
|
try {
|
|
97
|
-
const resolved =
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
if (!resolvedNorm.startsWith(pluginNorm + "/") && resolvedNorm !== pluginNorm) {
|
|
130
|
+
const resolved = localRequire.resolve("better-sqlite3", { paths: [pluginDir] });
|
|
131
|
+
const resolvedReal = fs.existsSync(resolved) ? fs.realpathSync.native(resolved) : resolved;
|
|
132
|
+
if (!isPathInside(pluginDir, resolvedReal)) {
|
|
101
133
|
api.logger.warn(`memos-local: better-sqlite3 resolved outside plugin dir: ${resolved}`);
|
|
102
134
|
return false;
|
|
103
135
|
}
|
|
104
|
-
|
|
136
|
+
localRequire(resolvedReal);
|
|
105
137
|
return true;
|
|
106
138
|
} catch {
|
|
107
139
|
return false;
|
|
@@ -114,13 +146,7 @@ const memosLocalPlugin = {
|
|
|
114
146
|
api.logger.warn(`memos-local: better-sqlite3 not found in ${pluginDir}, attempting auto-rebuild ...`);
|
|
115
147
|
|
|
116
148
|
try {
|
|
117
|
-
const
|
|
118
|
-
const rebuildResult = spawnSync("npm", ["rebuild", "better-sqlite3"], {
|
|
119
|
-
cwd: pluginDir,
|
|
120
|
-
stdio: "pipe",
|
|
121
|
-
shell: true,
|
|
122
|
-
timeout: 120_000,
|
|
123
|
-
});
|
|
149
|
+
const rebuildResult = runNpm(["rebuild", "better-sqlite3"]);
|
|
124
150
|
|
|
125
151
|
const stdout = rebuildResult.stdout?.toString() || "";
|
|
126
152
|
const stderr = rebuildResult.stderr?.toString() || "";
|
|
@@ -128,9 +154,9 @@ const memosLocalPlugin = {
|
|
|
128
154
|
if (stderr) api.logger.warn(`memos-local: rebuild stderr: ${stderr.slice(0, 500)}`);
|
|
129
155
|
|
|
130
156
|
if (rebuildResult.status === 0) {
|
|
131
|
-
Object.keys(
|
|
157
|
+
Object.keys(localRequire.cache)
|
|
132
158
|
.filter(k => k.includes("better-sqlite3") || k.includes("better_sqlite3"))
|
|
133
|
-
.forEach(k => delete
|
|
159
|
+
.forEach(k => delete localRequire.cache[k]);
|
|
134
160
|
sqliteReady = trySqliteLoad();
|
|
135
161
|
if (sqliteReady) {
|
|
136
162
|
api.logger.info("memos-local: better-sqlite3 auto-rebuild succeeded!");
|
|
@@ -222,7 +248,7 @@ const memosLocalPlugin = {
|
|
|
222
248
|
|
|
223
249
|
let pluginVersion = "0.0.0";
|
|
224
250
|
try {
|
|
225
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(
|
|
251
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(pluginDir, "package.json"), "utf-8"));
|
|
226
252
|
pluginVersion = pkg.version ?? pluginVersion;
|
|
227
253
|
} catch {}
|
|
228
254
|
const telemetry = new Telemetry(ctx.config.telemetry ?? {}, stateDir, pluginVersion, ctx.log, pluginDir);
|
|
@@ -406,7 +432,8 @@ const memosLocalPlugin = {
|
|
|
406
432
|
updatedAt: now,
|
|
407
433
|
});
|
|
408
434
|
} else if (ctx.config.sharing?.enabled && hubClient.userId) {
|
|
409
|
-
store.
|
|
435
|
+
const conn = store.getClientHubConnection();
|
|
436
|
+
store.upsertTeamSharedChunk(chunk.id, { hubMemoryId: memoryId, visibility, groupId, hubInstanceId: conn?.hubInstanceId ?? "" });
|
|
410
437
|
}
|
|
411
438
|
|
|
412
439
|
return { memoryId, visibility, groupId };
|
|
@@ -448,7 +475,7 @@ const memosLocalPlugin = {
|
|
|
448
475
|
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for group/all search." })),
|
|
449
476
|
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for group/all search." })),
|
|
450
477
|
}),
|
|
451
|
-
execute: trackTool("memory_search", async (_toolCallId: any, params: any) => {
|
|
478
|
+
execute: trackTool("memory_search", async (_toolCallId: any, params: any, context?: any) => {
|
|
452
479
|
const {
|
|
453
480
|
query,
|
|
454
481
|
scope: rawScope,
|
|
@@ -474,8 +501,8 @@ const memosLocalPlugin = {
|
|
|
474
501
|
}
|
|
475
502
|
const searchLimit = typeof maxResults === "number" ? Math.max(1, Math.min(20, Math.round(maxResults))) : 10;
|
|
476
503
|
|
|
477
|
-
const agentId = currentAgentId;
|
|
478
|
-
const ownerFilter = [
|
|
504
|
+
const agentId = context?.agentId ?? currentAgentId;
|
|
505
|
+
const ownerFilter = [`agent:${agentId}`, "public"];
|
|
479
506
|
const effectiveMaxResults = searchLimit;
|
|
480
507
|
ctx.log.debug(`memory_search query="${query}" maxResults=${effectiveMaxResults} minScore=${minScore ?? 0.45} role=${role ?? "all"} owner=agent:${agentId}`);
|
|
481
508
|
const result = await engine.search({ query, maxResults: effectiveMaxResults, minScore, role, ownerFilter });
|
|
@@ -713,14 +740,15 @@ const memosLocalPlugin = {
|
|
|
713
740
|
chunkId: Type.String({ description: "The chunkId from a memory_search hit" }),
|
|
714
741
|
window: Type.Optional(Type.Number({ description: "Context window ±N (default 2)" })),
|
|
715
742
|
}),
|
|
716
|
-
execute: trackTool("memory_timeline", async (_toolCallId: any, params: any) => {
|
|
717
|
-
|
|
743
|
+
execute: trackTool("memory_timeline", async (_toolCallId: any, params: any, context?: any) => {
|
|
744
|
+
const agentId = context?.agentId ?? currentAgentId;
|
|
745
|
+
ctx.log.debug(`memory_timeline called (agent=${agentId})`);
|
|
718
746
|
const { chunkId, window: win } = params as {
|
|
719
747
|
chunkId: string;
|
|
720
748
|
window?: number;
|
|
721
749
|
};
|
|
722
750
|
|
|
723
|
-
const ownerFilter = [`agent:${
|
|
751
|
+
const ownerFilter = [`agent:${agentId}`, "public"];
|
|
724
752
|
const anchorChunk = store.getChunkForOwners(chunkId, ownerFilter);
|
|
725
753
|
if (!anchorChunk) {
|
|
726
754
|
return {
|
|
@@ -778,7 +806,8 @@ const memosLocalPlugin = {
|
|
|
778
806
|
const { chunkId, maxChars } = params as { chunkId: string; maxChars?: number };
|
|
779
807
|
const limit = Math.min(maxChars ?? DEFAULTS.getMaxCharsDefault, DEFAULTS.getMaxCharsMax);
|
|
780
808
|
|
|
781
|
-
const
|
|
809
|
+
const agentId = context?.agentId ?? currentAgentId;
|
|
810
|
+
const ownerFilter = [`agent:${agentId}`, "public"];
|
|
782
811
|
const chunk = store.getChunkForOwners(chunkId, ownerFilter);
|
|
783
812
|
if (!chunk) {
|
|
784
813
|
return {
|
|
@@ -952,7 +981,8 @@ const memosLocalPlugin = {
|
|
|
952
981
|
}),
|
|
953
982
|
}) as any;
|
|
954
983
|
|
|
955
|
-
store.
|
|
984
|
+
const conn = store.getClientHubConnection();
|
|
985
|
+
store.markTaskShared(task.id, hubTaskId, chunks.length, visibility, groupId, conn?.hubInstanceId ?? "");
|
|
956
986
|
|
|
957
987
|
return {
|
|
958
988
|
content: [{ type: "text", text: `Shared task "${task.title}" with ${chunks.length} chunks to the hub.` }],
|
|
@@ -1781,7 +1811,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1781
1811
|
|
|
1782
1812
|
// ─── Auto-recall: inject relevant memories before agent starts ───
|
|
1783
1813
|
|
|
1784
|
-
api.on("
|
|
1814
|
+
api.on("before_prompt_build", async (event: { prompt?: string; messages?: unknown[] }, hookCtx?: { agentId?: string; sessionKey?: string }) => {
|
|
1785
1815
|
if (!allowPromptInjection) return {};
|
|
1786
1816
|
if (!event.prompt || event.prompt.length < 3) return;
|
|
1787
1817
|
|
|
@@ -2245,6 +2275,10 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
2245
2275
|
const shared = store.listLocalSharedTasks();
|
|
2246
2276
|
if (shared.length === 0) return;
|
|
2247
2277
|
|
|
2278
|
+
// Only sync tasks that have a hub_task_id (actively shared to remote)
|
|
2279
|
+
const conn = store.getClientHubConnection();
|
|
2280
|
+
const currentHubInstanceId = conn?.hubInstanceId || "";
|
|
2281
|
+
|
|
2248
2282
|
let hubClient: { hubUrl: string; userToken: string; userId: string } | undefined;
|
|
2249
2283
|
try {
|
|
2250
2284
|
hubClient = await resolveHubClient(store, ctx);
|
|
@@ -2254,6 +2288,8 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
2254
2288
|
const { v4: uuidv4 } = require("uuid");
|
|
2255
2289
|
|
|
2256
2290
|
for (const entry of shared) {
|
|
2291
|
+
if (!entry.hubTaskId) continue;
|
|
2292
|
+
if (currentHubInstanceId && entry.hubInstanceId && entry.hubInstanceId !== currentHubInstanceId) continue;
|
|
2257
2293
|
const task = store.getTask(entry.taskId);
|
|
2258
2294
|
if (!task) continue;
|
|
2259
2295
|
const chunks = store.getChunksByTask(entry.taskId);
|
|
@@ -2291,7 +2327,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
2291
2327
|
})),
|
|
2292
2328
|
}),
|
|
2293
2329
|
});
|
|
2294
|
-
store.markTaskShared(entry.taskId, entry.hubTaskId, chunks.length, entry.visibility, entry.groupId);
|
|
2330
|
+
store.markTaskShared(entry.taskId, entry.hubTaskId, chunks.length, entry.visibility, entry.groupId, currentHubInstanceId);
|
|
2295
2331
|
} catch (err) {
|
|
2296
2332
|
ctx.log.warn(`incremental sync failed for task=${entry.taskId}: ${err}`);
|
|
2297
2333
|
}
|
package/package.json
CHANGED
package/scripts/postinstall.cjs
CHANGED
|
@@ -23,6 +23,11 @@ function phase(n, title) {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const pluginDir = path.resolve(__dirname, "..");
|
|
26
|
+
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
27
|
+
|
|
28
|
+
function normalizePathForMatch(p) {
|
|
29
|
+
return path.resolve(p).replace(/^\\\\\?\\/, "").replace(/\\/g, "/").toLowerCase();
|
|
30
|
+
}
|
|
26
31
|
|
|
27
32
|
console.log(`
|
|
28
33
|
${CYAN}${BOLD}┌──────────────────────────────────────────────────┐
|
|
@@ -42,7 +47,8 @@ log(`Node: ${process.version} Platform: ${process.platform}-${process.arch}`);
|
|
|
42
47
|
* ═══════════════════════════════════════════════════════════ */
|
|
43
48
|
|
|
44
49
|
function cleanStaleArtifacts() {
|
|
45
|
-
const
|
|
50
|
+
const pluginDirNorm = normalizePathForMatch(pluginDir);
|
|
51
|
+
const isExtensionsDir = pluginDirNorm.includes("/.openclaw/extensions/");
|
|
46
52
|
if (!isExtensionsDir) return;
|
|
47
53
|
|
|
48
54
|
const pkgPath = path.join(pluginDir, "package.json");
|
|
@@ -133,10 +139,10 @@ function ensureDependencies() {
|
|
|
133
139
|
log("Running: npm install --omit=dev ...");
|
|
134
140
|
|
|
135
141
|
const startMs = Date.now();
|
|
136
|
-
const result = spawnSync(
|
|
142
|
+
const result = spawnSync(npmCmd, ["install", "--omit=dev"], {
|
|
137
143
|
cwd: pluginDir,
|
|
138
144
|
stdio: "pipe",
|
|
139
|
-
shell:
|
|
145
|
+
shell: false,
|
|
140
146
|
timeout: 120_000,
|
|
141
147
|
});
|
|
142
148
|
const elapsed = ((Date.now() - startMs) / 1000).toFixed(1);
|
|
@@ -223,8 +229,8 @@ function cleanupLegacy() {
|
|
|
223
229
|
newEntry.source = oldSource
|
|
224
230
|
.replace(/memos-lite-openclaw-plugin/g, "memos-local-openclaw-plugin")
|
|
225
231
|
.replace(/memos-lite/g, "memos-local-openclaw-plugin")
|
|
226
|
-
.replace(
|
|
227
|
-
.replace(
|
|
232
|
+
.replace(/[\\/]memos-local[\\/]/g, `${path.sep}memos-local-openclaw-plugin${path.sep}`)
|
|
233
|
+
.replace(/[\\/]memos-local$/g, `${path.sep}memos-local-openclaw-plugin`);
|
|
228
234
|
if (newEntry.source !== oldSource) {
|
|
229
235
|
log(`Updated source path: ${DIM}${oldSource}${RESET} → ${GREEN}${newEntry.source}${RESET}`);
|
|
230
236
|
cfgChanged = true;
|
|
@@ -384,6 +390,16 @@ if (sqliteBindingsExist()) {
|
|
|
384
390
|
warn("better-sqlite3 native bindings not found in plugin dir.");
|
|
385
391
|
log(`Searched in: ${DIM}${sqliteModulePath}/build/${RESET}`);
|
|
386
392
|
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
|
+
});
|
|
387
403
|
|
|
388
404
|
const startMs = Date.now();
|
|
389
405
|
const result = spawnSync("npm", ["rebuild", "better-sqlite3"], {
|
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 {
|
|
@@ -293,6 +306,12 @@ export async function autoJoinHub(
|
|
|
293
306
|
const existingIdentityKey = persisted?.identityKey || "";
|
|
294
307
|
|
|
295
308
|
log.info(`Joining Hub at ${hubUrl} as "${username}"...`);
|
|
309
|
+
let hubInstanceId = "";
|
|
310
|
+
try {
|
|
311
|
+
const info = await hubRequestJson(hubUrl, "", "/api/v1/hub/info", { method: "GET" }) as any;
|
|
312
|
+
hubInstanceId = String(info?.hubInstanceId ?? "");
|
|
313
|
+
} catch { /* best-effort */ }
|
|
314
|
+
|
|
296
315
|
const result = await hubRequestJson(hubUrl, "", "/api/v1/hub/join", {
|
|
297
316
|
method: "POST",
|
|
298
317
|
body: JSON.stringify({ teamToken, username, deviceName: hostname, clientIp, identityKey: existingIdentityKey }),
|
|
@@ -311,6 +330,7 @@ export async function autoJoinHub(
|
|
|
311
330
|
connectedAt: Date.now(),
|
|
312
331
|
identityKey: returnedIdentityKey,
|
|
313
332
|
lastKnownStatus: "pending",
|
|
333
|
+
hubInstanceId,
|
|
314
334
|
});
|
|
315
335
|
throw new PendingApprovalError(result.userId);
|
|
316
336
|
}
|
|
@@ -337,6 +357,7 @@ export async function autoJoinHub(
|
|
|
337
357
|
connectedAt: Date.now(),
|
|
338
358
|
identityKey: returnedIdentityKey,
|
|
339
359
|
lastKnownStatus: "active",
|
|
360
|
+
hubInstanceId,
|
|
340
361
|
});
|
|
341
362
|
return store.getClientHubConnection()!;
|
|
342
363
|
}
|
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 {
|
|
@@ -168,13 +169,23 @@ export class HubServer {
|
|
|
168
169
|
return this.authState.authSecret;
|
|
169
170
|
}
|
|
170
171
|
|
|
172
|
+
get hubInstanceId(): string {
|
|
173
|
+
return this.authState.hubInstanceId ?? "";
|
|
174
|
+
}
|
|
175
|
+
|
|
171
176
|
private loadAuthState(): HubAuthState {
|
|
172
177
|
try {
|
|
173
178
|
const raw = fs.readFileSync(this.authStatePath, "utf8");
|
|
174
179
|
const parsed = JSON.parse(raw) as HubAuthState;
|
|
175
|
-
if (parsed.authSecret)
|
|
180
|
+
if (parsed.authSecret) {
|
|
181
|
+
if (!parsed.hubInstanceId) {
|
|
182
|
+
parsed.hubInstanceId = randomUUID();
|
|
183
|
+
fs.writeFileSync(this.authStatePath, JSON.stringify(parsed, null, 2), "utf8");
|
|
184
|
+
}
|
|
185
|
+
return parsed;
|
|
186
|
+
}
|
|
176
187
|
} catch {}
|
|
177
|
-
const initial = { authSecret: randomBytes(32).toString("hex")
|
|
188
|
+
const initial: HubAuthState = { authSecret: randomBytes(32).toString("hex"), hubInstanceId: randomUUID() };
|
|
178
189
|
fs.mkdirSync(path.dirname(this.authStatePath), { recursive: true });
|
|
179
190
|
fs.writeFileSync(this.authStatePath, JSON.stringify(initial, null, 2), "utf8");
|
|
180
191
|
return initial;
|
|
@@ -215,19 +226,8 @@ export class HubServer {
|
|
|
215
226
|
});
|
|
216
227
|
}
|
|
217
228
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (!embedder) return;
|
|
221
|
-
const text = summary || content.slice(0, 500);
|
|
222
|
-
embedder.embed([text]).then((vectors) => {
|
|
223
|
-
if (vectors[0]) {
|
|
224
|
-
this.opts.store.upsertHubMemoryEmbedding(memoryId, new Float32Array(vectors[0]));
|
|
225
|
-
this.opts.log.info(`hub: embedded shared memory ${memoryId}`);
|
|
226
|
-
}
|
|
227
|
-
}).catch((err) => {
|
|
228
|
-
this.opts.log.warn(`hub: embedding shared memory failed: ${err}`);
|
|
229
|
-
});
|
|
230
|
-
}
|
|
229
|
+
// Hub memory embeddings are now computed on-the-fly at search time (two-stage retrieval)
|
|
230
|
+
// rather than cached in hub_memory_embeddings table, so no embedMemoryAsync needed.
|
|
231
231
|
|
|
232
232
|
private async handle(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
233
233
|
const url = new URL(req.url || "/", `http://127.0.0.1:${this.port}`);
|
|
@@ -238,6 +238,7 @@ export class HubServer {
|
|
|
238
238
|
teamName: this.teamName,
|
|
239
239
|
version: "0.0.0",
|
|
240
240
|
apiVersion: "v1",
|
|
241
|
+
hubInstanceId: this.hubInstanceId,
|
|
241
242
|
});
|
|
242
243
|
}
|
|
243
244
|
|
|
@@ -382,10 +383,13 @@ export class HubServer {
|
|
|
382
383
|
}
|
|
383
384
|
|
|
384
385
|
if (req.method === "POST" && routePath === "/api/v1/hub/leave") {
|
|
386
|
+
this.opts.store.deleteHubMemoriesByUser(auth.userId);
|
|
387
|
+
this.opts.store.deleteHubTasksByUser(auth.userId);
|
|
388
|
+
this.opts.store.deleteHubSkillsByUser(auth.userId);
|
|
385
389
|
this.userManager.markUserLeft(auth.userId);
|
|
386
390
|
this.knownOnlineUsers.delete(auth.userId);
|
|
387
391
|
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"`);
|
|
392
|
+
this.opts.log.info(`Hub: user "${auth.username}" (${auth.userId}) left voluntarily, resources cleaned, status set to "left"`);
|
|
389
393
|
return this.json(res, 200, { ok: true });
|
|
390
394
|
}
|
|
391
395
|
|
|
@@ -611,9 +615,7 @@ export class HubServer {
|
|
|
611
615
|
createdAt: existing?.createdAt ?? now,
|
|
612
616
|
updatedAt: now,
|
|
613
617
|
});
|
|
614
|
-
|
|
615
|
-
this.embedMemoryAsync(memoryId, String(m.summary || ""), String(m.content || ""));
|
|
616
|
-
}
|
|
618
|
+
// No embedding on share — hub memory vectors are computed on-the-fly at search time
|
|
617
619
|
if (!existing) {
|
|
618
620
|
this.notifyAdmins("resource_shared", "memory", String(m.summary || m.content?.slice(0, 60) || memoryId), auth.userId);
|
|
619
621
|
}
|
|
@@ -660,24 +662,38 @@ export class HubServer {
|
|
|
660
662
|
// Track which IDs are memories vs chunks
|
|
661
663
|
const memoryIdSet = new Set(memFtsHits.map(({ hit }) => hit.id));
|
|
662
664
|
|
|
663
|
-
//
|
|
665
|
+
// Two-stage retrieval: FTS candidates first, then embed + cosine rerank
|
|
664
666
|
let mergedIds: string[];
|
|
665
667
|
if (this.opts.embedder) {
|
|
666
668
|
try {
|
|
667
669
|
const [queryVec] = await this.opts.embedder.embed([query]);
|
|
668
670
|
if (queryVec) {
|
|
669
671
|
const allEmb = this.opts.store.getVisibleHubEmbeddings(auth.userId);
|
|
670
|
-
const memEmb = this.opts.store.getVisibleHubMemoryEmbeddings(auth.userId);
|
|
671
672
|
const scored: Array<{ id: string; score: number }> = [];
|
|
672
|
-
const cosineSim = (
|
|
673
|
+
const cosineSim = (a: Float32Array | number[], b: number[]) => {
|
|
673
674
|
let dot = 0, nA = 0, nB = 0;
|
|
674
|
-
|
|
675
|
-
|
|
675
|
+
const len = Math.min(a.length, b.length);
|
|
676
|
+
for (let i = 0; i < len; i++) {
|
|
677
|
+
dot += a[i] * b[i]; nA += a[i] * a[i]; nB += b[i] * b[i];
|
|
676
678
|
}
|
|
677
679
|
return nA > 0 && nB > 0 ? dot / (Math.sqrt(nA) * Math.sqrt(nB)) : 0;
|
|
678
680
|
};
|
|
679
|
-
for (const e of allEmb) scored.push({ id: e.chunkId, score: cosineSim(e.vector) });
|
|
680
|
-
|
|
681
|
+
for (const e of allEmb) scored.push({ id: e.chunkId, score: cosineSim(e.vector, queryVec) });
|
|
682
|
+
|
|
683
|
+
// Hub memories: embed FTS candidates on-the-fly instead of reading cached vectors
|
|
684
|
+
if (memFtsHits.length > 0) {
|
|
685
|
+
const memTexts = memFtsHits.map(({ hit }) => (hit.summary || hit.content || "").slice(0, 500));
|
|
686
|
+
try {
|
|
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 */ }
|
|
695
|
+
}
|
|
696
|
+
|
|
681
697
|
scored.sort((a, b) => b.score - a.score);
|
|
682
698
|
const topScored = scored.slice(0, maxResults * 2);
|
|
683
699
|
|