@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/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);
|
|
@@ -314,25 +340,11 @@ const memosLocalPlugin = {
|
|
|
314
340
|
try {
|
|
315
341
|
let outputText: string;
|
|
316
342
|
const det = result?.details;
|
|
317
|
-
if (det && Array.isArray(det.candidates)) {
|
|
318
|
-
outputText = JSON.stringify({
|
|
319
|
-
candidates: det.candidates,
|
|
320
|
-
filtered: det.hits ?? det.filtered ?? [],
|
|
321
|
-
});
|
|
322
|
-
} else if (det && det.local && det.hub) {
|
|
323
|
-
const localHits = det.local?.hits ?? [];
|
|
324
|
-
const hubHits = (det.hub?.hits ?? []).map((h: any) => ({
|
|
325
|
-
score: h.score ?? 0,
|
|
326
|
-
role: h.source?.role ?? h.role ?? "assistant",
|
|
327
|
-
summary: h.summary ?? "",
|
|
328
|
-
original_excerpt: h.excerpt ?? h.summary ?? "",
|
|
329
|
-
origin: "hub-remote",
|
|
330
|
-
ownerName: h.ownerName ?? "",
|
|
331
|
-
groupName: h.groupName ?? "",
|
|
332
|
-
}));
|
|
343
|
+
if (det && (Array.isArray(det.candidates) || Array.isArray(det.filtered))) {
|
|
333
344
|
outputText = JSON.stringify({
|
|
334
|
-
candidates: [
|
|
335
|
-
|
|
345
|
+
candidates: det.candidates ?? [],
|
|
346
|
+
hubCandidates: det.hubCandidates ?? [],
|
|
347
|
+
filtered: det.filtered ?? det.hits ?? [],
|
|
336
348
|
});
|
|
337
349
|
} else {
|
|
338
350
|
outputText = result?.content?.[0]?.text ?? JSON.stringify(result ?? "");
|
|
@@ -406,7 +418,8 @@ const memosLocalPlugin = {
|
|
|
406
418
|
updatedAt: now,
|
|
407
419
|
});
|
|
408
420
|
} else if (ctx.config.sharing?.enabled && hubClient.userId) {
|
|
409
|
-
store.
|
|
421
|
+
const conn = store.getClientHubConnection();
|
|
422
|
+
store.upsertTeamSharedChunk(chunk.id, { hubMemoryId: memoryId, visibility, groupId, hubInstanceId: conn?.hubInstanceId ?? "" });
|
|
410
423
|
}
|
|
411
424
|
|
|
412
425
|
return { memoryId, visibility, groupId };
|
|
@@ -448,7 +461,7 @@ const memosLocalPlugin = {
|
|
|
448
461
|
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for group/all search." })),
|
|
449
462
|
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for group/all search." })),
|
|
450
463
|
}),
|
|
451
|
-
execute: trackTool("memory_search", async (_toolCallId: any, params: any) => {
|
|
464
|
+
execute: trackTool("memory_search", async (_toolCallId: any, params: any, context?: any) => {
|
|
452
465
|
const {
|
|
453
466
|
query,
|
|
454
467
|
scope: rawScope,
|
|
@@ -474,14 +487,26 @@ const memosLocalPlugin = {
|
|
|
474
487
|
}
|
|
475
488
|
const searchLimit = typeof maxResults === "number" ? Math.max(1, Math.min(20, Math.round(maxResults))) : 10;
|
|
476
489
|
|
|
477
|
-
const agentId = currentAgentId;
|
|
478
|
-
const ownerFilter = [
|
|
490
|
+
const agentId = context?.agentId ?? currentAgentId;
|
|
491
|
+
const ownerFilter = [`agent:${agentId}`, "public"];
|
|
479
492
|
const effectiveMaxResults = searchLimit;
|
|
480
493
|
ctx.log.debug(`memory_search query="${query}" maxResults=${effectiveMaxResults} minScore=${minScore ?? 0.45} role=${role ?? "all"} owner=agent:${agentId}`);
|
|
481
|
-
const result = await engine.search({ query, maxResults: effectiveMaxResults, minScore, role, ownerFilter });
|
|
482
|
-
ctx.log.debug(`memory_search raw candidates: ${result.hits.length}`);
|
|
483
494
|
|
|
484
|
-
|
|
495
|
+
// ── Phase 1: Local search ∥ Hub search (parallel) ──
|
|
496
|
+
const localSearchP = engine.search({ query, maxResults: effectiveMaxResults, minScore, role, ownerFilter });
|
|
497
|
+
const hubSearchP = searchScope !== "local"
|
|
498
|
+
? hubSearchMemories(store, ctx, { query, maxResults: searchLimit, scope: searchScope as any, hubAddress, userToken })
|
|
499
|
+
.catch(() => ({ hits: [] as any[], meta: { totalCandidates: 0, searchedGroups: [] as string[], includedPublic: searchScope === "all" } }))
|
|
500
|
+
: Promise.resolve(null);
|
|
501
|
+
|
|
502
|
+
const [result, hubResult] = await Promise.all([localSearchP, hubSearchP]);
|
|
503
|
+
ctx.log.debug(`memory_search raw candidates: local=${result.hits.length}, hub=${hubResult?.hits?.length ?? 0}`);
|
|
504
|
+
|
|
505
|
+
// Split local results: pure-local vs hub-memory (Hub role's hub_memories mixed in by RecallEngine)
|
|
506
|
+
const localHits = result.hits.filter((h) => h.origin !== "hub-memory");
|
|
507
|
+
const hubLocalHits = result.hits.filter((h) => h.origin === "hub-memory");
|
|
508
|
+
|
|
509
|
+
const rawLocalCandidates = localHits.map((h) => ({
|
|
485
510
|
chunkId: h.ref.chunkId,
|
|
486
511
|
role: h.source.role,
|
|
487
512
|
score: h.score,
|
|
@@ -490,208 +515,156 @@ const memosLocalPlugin = {
|
|
|
490
515
|
origin: h.origin || "local",
|
|
491
516
|
}));
|
|
492
517
|
|
|
493
|
-
|
|
518
|
+
// Hub remote candidates (from HTTP call) + hub-memory candidates (from RecallEngine for Hub role)
|
|
519
|
+
const hubRemoteHits = hubResult?.hits ?? [];
|
|
520
|
+
const rawHubCandidates = [
|
|
521
|
+
...hubLocalHits.map((h) => ({
|
|
522
|
+
score: h.score,
|
|
523
|
+
role: h.source.role,
|
|
524
|
+
summary: h.summary,
|
|
525
|
+
original_excerpt: (h.original_excerpt ?? "").slice(0, 200),
|
|
526
|
+
origin: "hub-memory" as const,
|
|
527
|
+
ownerName: "",
|
|
528
|
+
groupName: "",
|
|
529
|
+
})),
|
|
530
|
+
...hubRemoteHits.map((h: any) => ({
|
|
531
|
+
score: h.score ?? 0,
|
|
532
|
+
role: h.source?.role ?? h.role ?? "assistant",
|
|
533
|
+
summary: h.summary ?? "",
|
|
534
|
+
original_excerpt: (h.excerpt ?? h.summary ?? "").slice(0, 200),
|
|
535
|
+
origin: "hub-remote" as const,
|
|
536
|
+
ownerName: h.ownerName ?? "",
|
|
537
|
+
groupName: h.groupName ?? "",
|
|
538
|
+
})),
|
|
539
|
+
];
|
|
540
|
+
|
|
541
|
+
if (localHits.length === 0 && rawHubCandidates.length === 0) {
|
|
494
542
|
return {
|
|
495
543
|
content: [{ type: "text", text: result.meta.note ?? "No relevant memories found." }],
|
|
496
|
-
details: { candidates: [], meta: result.meta },
|
|
544
|
+
details: { candidates: rawLocalCandidates, hubCandidates: [], filtered: [], meta: result.meta },
|
|
497
545
|
};
|
|
498
546
|
}
|
|
499
547
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
content: (h.original_excerpt ?? "").slice(0, 300),
|
|
507
|
-
time: h.source.ts ? new Date(h.source.ts).toISOString().slice(0, 16) : "",
|
|
508
|
-
}));
|
|
509
|
-
|
|
510
|
-
const filterResult = await summarizer.filterRelevant(query, candidates);
|
|
511
|
-
if (filterResult !== null) {
|
|
512
|
-
sufficient = filterResult.sufficient;
|
|
513
|
-
if (filterResult.relevant.length > 0) {
|
|
514
|
-
const indexSet = new Set(filterResult.relevant);
|
|
515
|
-
filteredHits = result.hits.filter((_, i) => indexSet.has(i + 1));
|
|
516
|
-
ctx.log.debug(`memory_search LLM filter: ${result.hits.length} → ${filteredHits.length} hits, sufficient=${sufficient}`);
|
|
517
|
-
} else if (searchScope === "local") {
|
|
518
|
-
return {
|
|
519
|
-
content: [{ type: "text", text: "No relevant memories found for this query." }],
|
|
520
|
-
details: { candidates: rawCandidates, filtered: [], meta: result.meta },
|
|
521
|
-
};
|
|
522
|
-
} else {
|
|
523
|
-
filteredHits = [];
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
const beforeDedup = filteredHits.length;
|
|
528
|
-
filteredHits = deduplicateHits(filteredHits);
|
|
529
|
-
ctx.log.debug(`memory_search dedup: ${beforeDedup} → ${filteredHits.length}`);
|
|
530
|
-
|
|
531
|
-
const localDetailsHits = filteredHits.map((h) => {
|
|
532
|
-
let effectiveTaskId = h.taskId;
|
|
533
|
-
if (effectiveTaskId) {
|
|
534
|
-
const t = store.getTask(effectiveTaskId);
|
|
535
|
-
if (t && t.status === "skipped") effectiveTaskId = null;
|
|
536
|
-
}
|
|
537
|
-
return {
|
|
538
|
-
ref: h.ref,
|
|
539
|
-
chunkId: h.ref.chunkId,
|
|
540
|
-
taskId: effectiveTaskId,
|
|
541
|
-
skillId: h.skillId,
|
|
548
|
+
// ── Phase 2: Merge all candidates → single LLM filter ──
|
|
549
|
+
const allHitsForFilter = [...localHits, ...hubLocalHits];
|
|
550
|
+
const hubRemoteForFilter = hubRemoteHits;
|
|
551
|
+
const mergedCandidates = [
|
|
552
|
+
...allHitsForFilter.map((h, i) => ({
|
|
553
|
+
index: i + 1,
|
|
542
554
|
role: h.source.role,
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
555
|
+
content: (h.original_excerpt ?? "").slice(0, 300),
|
|
556
|
+
time: h.source.ts ? new Date(h.source.ts).toISOString().slice(0, 16) : "",
|
|
557
|
+
})),
|
|
558
|
+
...hubRemoteForFilter.map((h: any, i: number) => ({
|
|
559
|
+
index: allHitsForFilter.length + i + 1,
|
|
560
|
+
role: (h.source?.role || "assistant") as string,
|
|
561
|
+
content: (h.summary || h.excerpt || "").slice(0, 300),
|
|
562
|
+
time: h.source?.ts ? new Date(h.source.ts).toISOString().slice(0, 16) : "",
|
|
563
|
+
})),
|
|
564
|
+
];
|
|
565
|
+
|
|
566
|
+
let filteredLocalHits = allHitsForFilter;
|
|
567
|
+
let filteredHubRemoteHits = hubRemoteForFilter;
|
|
568
|
+
let sufficient = false;
|
|
548
569
|
|
|
549
|
-
if (
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
role: h.source.role,
|
|
563
|
-
content: (h.original_excerpt ?? "").slice(0, 300),
|
|
564
|
-
time: h.source.ts ? new Date(h.source.ts).toISOString().slice(0, 16) : "",
|
|
565
|
-
}));
|
|
566
|
-
const mergedCandidates = [...localCandidatesForMerge, ...hubCandidates];
|
|
567
|
-
const mergedFilter = await summarizer.filterRelevant(query, mergedCandidates);
|
|
568
|
-
if (mergedFilter !== null && mergedFilter.relevant.length > 0) {
|
|
569
|
-
const relevantSet = new Set(mergedFilter.relevant);
|
|
570
|
-
const hubStartIdx = filteredHits.length + 1;
|
|
571
|
-
filteredHits = filteredHits.filter((_, i) => relevantSet.has(i + 1));
|
|
572
|
-
filteredHubHits = hub.hits.filter((_, i) => relevantSet.has(hubStartIdx + i));
|
|
573
|
-
ctx.log.debug(`memory_search LLM filter (merged): local ${localCandidatesForMerge.length}→${filteredHits.length}, hub ${hub.hits.length}→${filteredHubHits.length}`);
|
|
570
|
+
if (mergedCandidates.length > 0) {
|
|
571
|
+
const filterResult = await summarizer.filterRelevant(query, mergedCandidates);
|
|
572
|
+
if (filterResult !== null) {
|
|
573
|
+
sufficient = filterResult.sufficient;
|
|
574
|
+
if (filterResult.relevant.length > 0) {
|
|
575
|
+
const relevantSet = new Set(filterResult.relevant);
|
|
576
|
+
const hubStartIdx = allHitsForFilter.length + 1;
|
|
577
|
+
filteredLocalHits = allHitsForFilter.filter((_, i) => relevantSet.has(i + 1));
|
|
578
|
+
filteredHubRemoteHits = hubRemoteForFilter.filter((_: any, i: number) => relevantSet.has(hubStartIdx + i));
|
|
579
|
+
ctx.log.debug(`memory_search LLM filter: merged ${mergedCandidates.length} → local ${filteredLocalHits.length}, hub ${filteredHubRemoteHits.length}`);
|
|
580
|
+
} else {
|
|
581
|
+
filteredLocalHits = [];
|
|
582
|
+
filteredHubRemoteHits = [];
|
|
574
583
|
}
|
|
575
584
|
}
|
|
576
|
-
|
|
577
|
-
const originLabel = (h: SearchHit) => {
|
|
578
|
-
if (h.origin === "hub-memory") return " [团队缓存]";
|
|
579
|
-
if (h.origin === "local-shared") return " [本机共享]";
|
|
580
|
-
return "";
|
|
581
|
-
};
|
|
582
|
-
const localText = filteredHits.length > 0
|
|
583
|
-
? filteredHits.map((h, i) => {
|
|
584
|
-
const excerpt = h.original_excerpt.length > 220 ? h.original_excerpt.slice(0, 217) + "..." : h.original_excerpt;
|
|
585
|
-
return `${i + 1}. [${h.source.role}]${originLabel(h)} ${excerpt}`;
|
|
586
|
-
}).join("\n")
|
|
587
|
-
: "(none)";
|
|
588
|
-
const hubText = filteredHubHits.length > 0
|
|
589
|
-
? filteredHubHits.map((h, i) => `${i + 1}. [${h.ownerName}] [团队] ${h.summary}${h.groupName ? ` (${h.groupName})` : ""}`).join("\n")
|
|
590
|
-
: "(none)";
|
|
591
|
-
|
|
592
|
-
const localDetailsFiltered = filteredHits.map((h) => {
|
|
593
|
-
let effectiveTaskId = h.taskId;
|
|
594
|
-
if (effectiveTaskId) {
|
|
595
|
-
const t = store.getTask(effectiveTaskId);
|
|
596
|
-
if (t && t.status === "skipped") effectiveTaskId = null;
|
|
597
|
-
}
|
|
598
|
-
return {
|
|
599
|
-
ref: h.ref,
|
|
600
|
-
chunkId: h.ref.chunkId,
|
|
601
|
-
taskId: effectiveTaskId,
|
|
602
|
-
skillId: h.skillId,
|
|
603
|
-
role: h.source.role,
|
|
604
|
-
score: h.score,
|
|
605
|
-
summary: h.summary,
|
|
606
|
-
origin: h.origin,
|
|
607
|
-
};
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
return {
|
|
611
|
-
content: [{
|
|
612
|
-
type: "text",
|
|
613
|
-
text: `Local results:\n${localText}\n\nHub results:\n${hubText}`,
|
|
614
|
-
}],
|
|
615
|
-
details: {
|
|
616
|
-
local: { hits: localDetailsFiltered, meta: result.meta },
|
|
617
|
-
hub: { ...hub, hits: filteredHubHits },
|
|
618
|
-
},
|
|
619
|
-
};
|
|
620
585
|
}
|
|
621
586
|
|
|
622
|
-
|
|
587
|
+
const beforeDedup = filteredLocalHits.length;
|
|
588
|
+
filteredLocalHits = deduplicateHits(filteredLocalHits);
|
|
589
|
+
ctx.log.debug(`memory_search dedup: ${beforeDedup} → ${filteredLocalHits.length}`);
|
|
590
|
+
|
|
591
|
+
if (filteredLocalHits.length === 0 && filteredHubRemoteHits.length === 0) {
|
|
623
592
|
return {
|
|
624
593
|
content: [{ type: "text", text: "No relevant memories found for this query." }],
|
|
625
|
-
details: { candidates:
|
|
594
|
+
details: { candidates: rawLocalCandidates, hubCandidates: rawHubCandidates, filtered: [], meta: result.meta },
|
|
626
595
|
};
|
|
627
596
|
}
|
|
628
597
|
|
|
598
|
+
// ── Phase 3: Build response text ──
|
|
629
599
|
const originTag = (o?: string) => {
|
|
630
600
|
if (o === "local-shared") return " [本机共享]";
|
|
631
601
|
if (o === "hub-memory") return " [团队缓存]";
|
|
632
602
|
if (o === "hub-remote") return " [团队]";
|
|
633
603
|
return "";
|
|
634
604
|
};
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
const
|
|
638
|
-
|
|
605
|
+
|
|
606
|
+
const localLines = filteredLocalHits.map((h, i) => {
|
|
607
|
+
const excerpt = h.original_excerpt.length > 220 ? h.original_excerpt.slice(0, 217) + "..." : h.original_excerpt;
|
|
608
|
+
const parts = [`${i + 1}. [${h.source.role}]${originTag(h.origin)} ${excerpt}`];
|
|
639
609
|
parts.push(` chunkId="${h.ref.chunkId}"`);
|
|
640
610
|
if (h.taskId) {
|
|
641
611
|
const task = store.getTask(h.taskId);
|
|
642
|
-
if (task && task.status !== "skipped") {
|
|
643
|
-
parts.push(` task_id="${h.taskId}"`);
|
|
644
|
-
}
|
|
612
|
+
if (task && task.status !== "skipped") parts.push(` task_id="${h.taskId}"`);
|
|
645
613
|
}
|
|
646
614
|
return parts.join("\n");
|
|
647
615
|
});
|
|
648
616
|
|
|
617
|
+
const hubLines = filteredHubRemoteHits.map((h: any, i: number) =>
|
|
618
|
+
`${i + 1}. [${h.ownerName ?? "team"}] [团队] ${h.summary ?? ""}${h.groupName ? ` (${h.groupName})` : ""}`
|
|
619
|
+
);
|
|
620
|
+
|
|
649
621
|
let tipsText = "";
|
|
650
622
|
if (!sufficient) {
|
|
651
|
-
const hasTask =
|
|
623
|
+
const hasTask = filteredLocalHits.some((h) => {
|
|
652
624
|
if (!h.taskId) return false;
|
|
653
625
|
const t = store.getTask(h.taskId);
|
|
654
626
|
return t && t.status !== "skipped";
|
|
655
627
|
});
|
|
656
|
-
|
|
657
628
|
const tips: string[] = [];
|
|
658
629
|
if (hasTask) {
|
|
659
630
|
tips.push("→ call task_summary(taskId) for full task context");
|
|
660
631
|
tips.push("→ call skill_get(taskId=...) if the task has a proven experience guide");
|
|
661
632
|
}
|
|
662
633
|
tips.push("→ call memory_timeline(chunkId) to expand surrounding conversation");
|
|
663
|
-
|
|
664
|
-
if (tips.length > 0) {
|
|
665
|
-
tipsText = "\n\nThese memories may not be enough. You can fetch more context:\n" + tips.join("\n");
|
|
666
|
-
}
|
|
634
|
+
if (tips.length > 0) tipsText = "\n\nThese memories may not be enough. You can fetch more context:\n" + tips.join("\n");
|
|
667
635
|
}
|
|
668
636
|
|
|
637
|
+
const localText = localLines.length > 0 ? localLines.join("\n\n") : "(none)";
|
|
638
|
+
const hubText = hubLines.length > 0 ? hubLines.join("\n") : "(none)";
|
|
639
|
+
const totalFiltered = filteredLocalHits.length + filteredHubRemoteHits.length;
|
|
640
|
+
const responseText = filteredHubRemoteHits.length > 0
|
|
641
|
+
? `Found ${totalFiltered} relevant memories:\n\nLocal results:\n${localText}\n\nHub results:\n${hubText}${tipsText}`
|
|
642
|
+
: `Found ${totalFiltered} relevant memories:\n\n${localText}${tipsText}`;
|
|
643
|
+
|
|
644
|
+
const filteredDetails = [
|
|
645
|
+
...filteredLocalHits.map((h) => {
|
|
646
|
+
let effectiveTaskId = h.taskId;
|
|
647
|
+
if (effectiveTaskId) { const t = store.getTask(effectiveTaskId); if (t && t.status === "skipped") effectiveTaskId = null; }
|
|
648
|
+
return {
|
|
649
|
+
chunkId: h.ref.chunkId, taskId: effectiveTaskId, skillId: h.skillId,
|
|
650
|
+
role: h.source.role, score: h.score, summary: h.summary,
|
|
651
|
+
original_excerpt: (h.original_excerpt ?? "").slice(0, 200), origin: h.origin || "local",
|
|
652
|
+
};
|
|
653
|
+
}),
|
|
654
|
+
...filteredHubRemoteHits.map((h: any) => ({
|
|
655
|
+
chunkId: "", taskId: null, skillId: null,
|
|
656
|
+
role: h.source?.role ?? h.role ?? "assistant", score: h.score ?? 0,
|
|
657
|
+
summary: h.summary ?? "", original_excerpt: (h.excerpt ?? h.summary ?? "").slice(0, 200),
|
|
658
|
+
origin: "hub-remote", ownerName: h.ownerName ?? "", groupName: h.groupName ?? "",
|
|
659
|
+
})),
|
|
660
|
+
];
|
|
661
|
+
|
|
669
662
|
return {
|
|
670
|
-
content: [
|
|
671
|
-
{
|
|
672
|
-
type: "text",
|
|
673
|
-
text: `Found ${filteredHits.length} relevant memories:\n\n${lines.join("\n\n")}${tipsText}`,
|
|
674
|
-
},
|
|
675
|
-
],
|
|
663
|
+
content: [{ type: "text", text: responseText }],
|
|
676
664
|
details: {
|
|
677
|
-
candidates:
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
if (effectiveTaskId) {
|
|
681
|
-
const t = store.getTask(effectiveTaskId);
|
|
682
|
-
if (t && t.status === "skipped") effectiveTaskId = null;
|
|
683
|
-
}
|
|
684
|
-
return {
|
|
685
|
-
chunkId: h.ref.chunkId,
|
|
686
|
-
taskId: effectiveTaskId,
|
|
687
|
-
skillId: h.skillId,
|
|
688
|
-
role: h.source.role,
|
|
689
|
-
score: h.score,
|
|
690
|
-
summary: h.summary,
|
|
691
|
-
original_excerpt: (h.original_excerpt ?? "").slice(0, 200),
|
|
692
|
-
origin: h.origin || "local",
|
|
693
|
-
};
|
|
694
|
-
}),
|
|
665
|
+
candidates: rawLocalCandidates,
|
|
666
|
+
hubCandidates: rawHubCandidates,
|
|
667
|
+
filtered: filteredDetails,
|
|
695
668
|
meta: result.meta,
|
|
696
669
|
},
|
|
697
670
|
};
|
|
@@ -713,14 +686,15 @@ const memosLocalPlugin = {
|
|
|
713
686
|
chunkId: Type.String({ description: "The chunkId from a memory_search hit" }),
|
|
714
687
|
window: Type.Optional(Type.Number({ description: "Context window ±N (default 2)" })),
|
|
715
688
|
}),
|
|
716
|
-
execute: trackTool("memory_timeline", async (_toolCallId: any, params: any) => {
|
|
717
|
-
|
|
689
|
+
execute: trackTool("memory_timeline", async (_toolCallId: any, params: any, context?: any) => {
|
|
690
|
+
const agentId = context?.agentId ?? currentAgentId;
|
|
691
|
+
ctx.log.debug(`memory_timeline called (agent=${agentId})`);
|
|
718
692
|
const { chunkId, window: win } = params as {
|
|
719
693
|
chunkId: string;
|
|
720
694
|
window?: number;
|
|
721
695
|
};
|
|
722
696
|
|
|
723
|
-
const ownerFilter = [`agent:${
|
|
697
|
+
const ownerFilter = [`agent:${agentId}`, "public"];
|
|
724
698
|
const anchorChunk = store.getChunkForOwners(chunkId, ownerFilter);
|
|
725
699
|
if (!anchorChunk) {
|
|
726
700
|
return {
|
|
@@ -778,7 +752,8 @@ const memosLocalPlugin = {
|
|
|
778
752
|
const { chunkId, maxChars } = params as { chunkId: string; maxChars?: number };
|
|
779
753
|
const limit = Math.min(maxChars ?? DEFAULTS.getMaxCharsDefault, DEFAULTS.getMaxCharsMax);
|
|
780
754
|
|
|
781
|
-
const
|
|
755
|
+
const agentId = context?.agentId ?? currentAgentId;
|
|
756
|
+
const ownerFilter = [`agent:${agentId}`, "public"];
|
|
782
757
|
const chunk = store.getChunkForOwners(chunkId, ownerFilter);
|
|
783
758
|
if (!chunk) {
|
|
784
759
|
return {
|
|
@@ -952,7 +927,8 @@ const memosLocalPlugin = {
|
|
|
952
927
|
}),
|
|
953
928
|
}) as any;
|
|
954
929
|
|
|
955
|
-
store.
|
|
930
|
+
const conn = store.getClientHubConnection();
|
|
931
|
+
store.markTaskShared(task.id, hubTaskId, chunks.length, visibility, groupId, conn?.hubInstanceId ?? "");
|
|
956
932
|
|
|
957
933
|
return {
|
|
958
934
|
content: [{ type: "text", text: `Shared task "${task.title}" with ${chunks.length} chunks to the hub.` }],
|
|
@@ -1609,16 +1585,32 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1609
1585
|
};
|
|
1610
1586
|
}
|
|
1611
1587
|
|
|
1612
|
-
|
|
1613
|
-
|
|
1588
|
+
let filteredLocal = localHits;
|
|
1589
|
+
let filteredHub = hub.hits;
|
|
1590
|
+
if (localHits.length > 0 && hub.hits.length > 0) {
|
|
1591
|
+
const allCandidates = [
|
|
1592
|
+
...localHits.map((h, i) => ({ index: i + 1, role: "skill" as const, content: `[${h.name}] ${h.description.slice(0, 200)}` })),
|
|
1593
|
+
...hub.hits.map((h, i) => ({ index: localHits.length + i + 1, role: "skill" as const, content: `[${h.name}] ${h.description.slice(0, 200)}` })),
|
|
1594
|
+
];
|
|
1595
|
+
const mergedFilter = await summarizer.filterRelevant(skillQuery, allCandidates);
|
|
1596
|
+
if (mergedFilter !== null && mergedFilter.relevant.length > 0) {
|
|
1597
|
+
const relevantSet = new Set(mergedFilter.relevant);
|
|
1598
|
+
filteredLocal = localHits.filter((_, i) => relevantSet.has(i + 1));
|
|
1599
|
+
filteredHub = hub.hits.filter((_, i) => relevantSet.has(localHits.length + i + 1));
|
|
1600
|
+
ctx.log.debug(`skill_search LLM filter (merged): local ${localHits.length}→${filteredLocal.length}, hub ${hub.hits.length}→${filteredHub.length}`);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
const localText = filteredLocal.length > 0
|
|
1605
|
+
? filteredLocal.map((h, i) => `${i + 1}. [${h.name}] ${h.description.slice(0, 150)}${h.visibility === "public" ? " (shared to local agents)" : ""}`).join("\n")
|
|
1614
1606
|
: "(none)";
|
|
1615
|
-
const hubText =
|
|
1616
|
-
?
|
|
1607
|
+
const hubText = filteredHub.length > 0
|
|
1608
|
+
? filteredHub.map((h, i) => `${i + 1}. [${h.name}] ${h.description.slice(0, 150)} (${h.visibility}${h.groupName ? `:${h.groupName}` : ""}, owner=${h.ownerName})`).join("\n")
|
|
1617
1609
|
: "(none)";
|
|
1618
1610
|
|
|
1619
1611
|
return {
|
|
1620
1612
|
content: [{ type: "text", text: `Local skills:\n${localText}\n\nHub skills:\n${hubText}` }],
|
|
1621
|
-
details: { query: skillQuery, scope: rawScope, local: { hits:
|
|
1613
|
+
details: { query: skillQuery, scope: rawScope, local: { hits: filteredLocal }, hub: { hits: filteredHub } },
|
|
1622
1614
|
};
|
|
1623
1615
|
}
|
|
1624
1616
|
|
|
@@ -1781,7 +1773,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1781
1773
|
|
|
1782
1774
|
// ─── Auto-recall: inject relevant memories before agent starts ───
|
|
1783
1775
|
|
|
1784
|
-
api.on("
|
|
1776
|
+
api.on("before_prompt_build", async (event: { prompt?: string; messages?: unknown[] }, hookCtx?: { agentId?: string; sessionKey?: string }) => {
|
|
1785
1777
|
if (!allowPromptInjection) return {};
|
|
1786
1778
|
if (!event.prompt || event.prompt.length < 3) return;
|
|
1787
1779
|
|
|
@@ -1823,46 +1815,53 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1823
1815
|
}
|
|
1824
1816
|
ctx.log.debug(`auto-recall: query="${query.slice(0, 80)}"`);
|
|
1825
1817
|
|
|
1826
|
-
|
|
1818
|
+
// ── Phase 1: Local search ∥ Hub search (parallel) ──
|
|
1819
|
+
const arLocalP = engine.search({ query, maxResults: 10, minScore: 0.45, ownerFilter: recallOwnerFilter });
|
|
1820
|
+
const arHubP = ctx.config?.sharing?.enabled
|
|
1821
|
+
? hubSearchMemories(store, ctx, { query, maxResults: 10, scope: "all" })
|
|
1822
|
+
.catch((err: any) => { ctx.log.debug(`auto-recall: hub search failed (${err})`); return { hits: [] as any[], meta: {} }; })
|
|
1823
|
+
: Promise.resolve({ hits: [] as any[], meta: {} });
|
|
1824
|
+
|
|
1825
|
+
const [result, arHubResult] = await Promise.all([arLocalP, arHubP]);
|
|
1826
|
+
|
|
1827
|
+
const localHits = result.hits.filter((h) => h.origin !== "hub-memory");
|
|
1828
|
+
const hubLocalHits = result.hits.filter((h) => h.origin === "hub-memory");
|
|
1829
|
+
const hubRemoteHits: SearchHit[] = (arHubResult.hits ?? []).map((h: any) => ({
|
|
1830
|
+
summary: h.summary,
|
|
1831
|
+
original_excerpt: h.excerpt || h.summary,
|
|
1832
|
+
ref: { sessionKey: "", chunkId: h.remoteHitId ?? "", turnId: "", seq: 0 },
|
|
1833
|
+
score: 0.9,
|
|
1834
|
+
taskId: null,
|
|
1835
|
+
skillId: null,
|
|
1836
|
+
origin: "hub-remote" as const,
|
|
1837
|
+
source: { ts: h.source?.ts, role: h.source?.role ?? "assistant", sessionKey: "" },
|
|
1838
|
+
ownerName: h.ownerName,
|
|
1839
|
+
groupName: h.groupName,
|
|
1840
|
+
}));
|
|
1841
|
+
const allHubHits = [...hubLocalHits, ...hubRemoteHits];
|
|
1827
1842
|
|
|
1828
|
-
|
|
1829
|
-
const hubFallback = async (): Promise<SearchHit[]> => {
|
|
1830
|
-
if (!ctx.config?.sharing?.enabled) return [];
|
|
1831
|
-
try {
|
|
1832
|
-
const hubResult = await hubSearchMemories(store, ctx, { query, maxResults: 10, scope: "all" });
|
|
1833
|
-
if (hubResult.hits.length === 0) return [];
|
|
1834
|
-
ctx.log.debug(`auto-recall: hub fallback returned ${hubResult.hits.length} hit(s)`);
|
|
1835
|
-
return hubResult.hits.map((h) => ({
|
|
1836
|
-
summary: h.summary,
|
|
1837
|
-
original_excerpt: h.excerpt || h.summary,
|
|
1838
|
-
ref: { sessionKey: "", chunkId: h.remoteHitId, turnId: "", seq: 0 },
|
|
1839
|
-
score: 0.9,
|
|
1840
|
-
taskId: null,
|
|
1841
|
-
skillId: null,
|
|
1842
|
-
origin: "hub-remote" as const,
|
|
1843
|
-
source: { ts: h.source.ts, role: h.source.role, sessionKey: "" },
|
|
1844
|
-
}));
|
|
1845
|
-
} catch (err) {
|
|
1846
|
-
ctx.log.debug(`auto-recall: hub fallback failed (${err})`);
|
|
1847
|
-
return [];
|
|
1848
|
-
}
|
|
1849
|
-
};
|
|
1843
|
+
ctx.log.debug(`auto-recall: local=${localHits.length}, hub-memory=${hubLocalHits.length}, hub-remote=${hubRemoteHits.length}`);
|
|
1850
1844
|
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1845
|
+
const rawLocalCandidates = localHits.map((h) => ({
|
|
1846
|
+
score: h.score, role: h.source.role, summary: h.summary,
|
|
1847
|
+
content: (h.original_excerpt ?? "").slice(0, 200), origin: h.origin || "local",
|
|
1848
|
+
}));
|
|
1849
|
+
const rawHubCandidates = allHubHits.map((h) => ({
|
|
1850
|
+
score: h.score, role: h.source.role, summary: h.summary,
|
|
1851
|
+
content: (h.original_excerpt ?? "").slice(0, 200), origin: h.origin || "hub-remote",
|
|
1852
|
+
ownerName: (h as any).ownerName ?? "", groupName: (h as any).groupName ?? "",
|
|
1853
|
+
}));
|
|
1854
|
+
|
|
1855
|
+
const allRawHits = [...localHits, ...allHubHits];
|
|
1856
|
+
|
|
1857
|
+
if (allRawHits.length === 0) {
|
|
1860
1858
|
ctx.log.debug("auto-recall: no memory candidates found");
|
|
1861
1859
|
const dur = performance.now() - recallT0;
|
|
1862
1860
|
store.recordToolCall("memory_search", dur, true);
|
|
1863
|
-
store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({
|
|
1861
|
+
store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({
|
|
1862
|
+
candidates: rawLocalCandidates, hubCandidates: rawHubCandidates, filtered: [],
|
|
1863
|
+
}), dur, true);
|
|
1864
1864
|
|
|
1865
|
-
// Even without memory hits, try skill recall
|
|
1866
1865
|
const skillAutoRecallEarly = ctx.config.skillEvolution?.autoRecallSkills ?? DEFAULTS.skillAutoRecall;
|
|
1867
1866
|
if (skillAutoRecallEarly) {
|
|
1868
1867
|
try {
|
|
@@ -1902,59 +1901,44 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1902
1901
|
return;
|
|
1903
1902
|
}
|
|
1904
1903
|
|
|
1905
|
-
|
|
1904
|
+
// ── Phase 2: Merge all → single LLM filter ──
|
|
1905
|
+
const mergedForFilter = allRawHits.map((h, i) => ({
|
|
1906
1906
|
index: i + 1,
|
|
1907
1907
|
role: h.source.role,
|
|
1908
1908
|
content: (h.original_excerpt ?? "").slice(0, 300),
|
|
1909
1909
|
time: h.source.ts ? new Date(h.source.ts).toISOString().slice(0, 16) : "",
|
|
1910
1910
|
}));
|
|
1911
1911
|
|
|
1912
|
-
let filteredHits =
|
|
1912
|
+
let filteredHits = allRawHits;
|
|
1913
1913
|
let sufficient = false;
|
|
1914
1914
|
|
|
1915
|
-
const filterResult = await summarizer.filterRelevant(query,
|
|
1915
|
+
const filterResult = await summarizer.filterRelevant(query, mergedForFilter);
|
|
1916
1916
|
if (filterResult !== null) {
|
|
1917
1917
|
sufficient = filterResult.sufficient;
|
|
1918
1918
|
if (filterResult.relevant.length > 0) {
|
|
1919
1919
|
const indexSet = new Set(filterResult.relevant);
|
|
1920
|
-
filteredHits =
|
|
1920
|
+
filteredHits = allRawHits.filter((_, i) => indexSet.has(i + 1));
|
|
1921
1921
|
} else {
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
const
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
if (query.length > 50) {
|
|
1935
|
-
const noRecallHint =
|
|
1936
|
-
"## Memory system — ACTION REQUIRED\n\n" +
|
|
1937
|
-
"Auto-recall found no relevant results for a long query. " +
|
|
1938
|
-
"You MUST call `memory_search` now with a shortened query (2-5 key words) before answering. " +
|
|
1939
|
-
"Do NOT skip this step. Do NOT answer without searching first.";
|
|
1940
|
-
return { prependContext: noRecallHint };
|
|
1941
|
-
}
|
|
1942
|
-
return;
|
|
1922
|
+
const dur = performance.now() - recallT0;
|
|
1923
|
+
store.recordToolCall("memory_search", dur, true);
|
|
1924
|
+
store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({
|
|
1925
|
+
candidates: rawLocalCandidates, hubCandidates: rawHubCandidates, filtered: [],
|
|
1926
|
+
}), dur, true);
|
|
1927
|
+
if (query.length > 50) {
|
|
1928
|
+
const noRecallHint =
|
|
1929
|
+
"## Memory system — ACTION REQUIRED\n\n" +
|
|
1930
|
+
"Auto-recall found no relevant results for a long query. " +
|
|
1931
|
+
"You MUST call `memory_search` now with a shortened query (2-5 key words) before answering. " +
|
|
1932
|
+
"Do NOT skip this step. Do NOT answer without searching first.";
|
|
1933
|
+
return { prependContext: noRecallHint };
|
|
1943
1934
|
}
|
|
1944
|
-
|
|
1945
|
-
}
|
|
1946
|
-
|
|
1947
|
-
if (!sufficient && filteredHits.length > 0 && ctx.config?.sharing?.enabled) {
|
|
1948
|
-
const hubSupp = await hubFallback();
|
|
1949
|
-
if (hubSupp.length > 0) {
|
|
1950
|
-
ctx.log.debug(`auto-recall: local insufficient, supplementing with ${hubSupp.length} hub hit(s)`);
|
|
1951
|
-
filteredHits.push(...hubSupp);
|
|
1935
|
+
return;
|
|
1952
1936
|
}
|
|
1953
1937
|
}
|
|
1954
1938
|
|
|
1955
1939
|
const beforeDedup = filteredHits.length;
|
|
1956
1940
|
filteredHits = deduplicateHits(filteredHits);
|
|
1957
|
-
ctx.log.debug(`auto-recall: ${
|
|
1941
|
+
ctx.log.debug(`auto-recall: merged ${allRawHits.length} → ${beforeDedup} relevant → ${filteredHits.length} after dedup, sufficient=${sufficient}`);
|
|
1958
1942
|
|
|
1959
1943
|
const lines = filteredHits.map((h, i) => {
|
|
1960
1944
|
const excerpt = h.original_excerpt;
|
|
@@ -2068,8 +2052,9 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
2068
2052
|
const recallDur = performance.now() - recallT0;
|
|
2069
2053
|
store.recordToolCall("memory_search", recallDur, true);
|
|
2070
2054
|
store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({
|
|
2071
|
-
candidates:
|
|
2072
|
-
|
|
2055
|
+
candidates: rawLocalCandidates,
|
|
2056
|
+
hubCandidates: rawHubCandidates,
|
|
2057
|
+
filtered: filteredHits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt, origin: h.origin || "local" })),
|
|
2073
2058
|
}), recallDur, true);
|
|
2074
2059
|
telemetry.trackAutoRecall(filteredHits.length, recallDur);
|
|
2075
2060
|
|
|
@@ -2245,6 +2230,10 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
2245
2230
|
const shared = store.listLocalSharedTasks();
|
|
2246
2231
|
if (shared.length === 0) return;
|
|
2247
2232
|
|
|
2233
|
+
// Only sync tasks that have a hub_task_id (actively shared to remote)
|
|
2234
|
+
const conn = store.getClientHubConnection();
|
|
2235
|
+
const currentHubInstanceId = conn?.hubInstanceId || "";
|
|
2236
|
+
|
|
2248
2237
|
let hubClient: { hubUrl: string; userToken: string; userId: string } | undefined;
|
|
2249
2238
|
try {
|
|
2250
2239
|
hubClient = await resolveHubClient(store, ctx);
|
|
@@ -2254,6 +2243,8 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
2254
2243
|
const { v4: uuidv4 } = require("uuid");
|
|
2255
2244
|
|
|
2256
2245
|
for (const entry of shared) {
|
|
2246
|
+
if (!entry.hubTaskId) continue;
|
|
2247
|
+
if (currentHubInstanceId && entry.hubInstanceId && entry.hubInstanceId !== currentHubInstanceId) continue;
|
|
2257
2248
|
const task = store.getTask(entry.taskId);
|
|
2258
2249
|
if (!task) continue;
|
|
2259
2250
|
const chunks = store.getChunksByTask(entry.taskId);
|
|
@@ -2291,7 +2282,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
2291
2282
|
})),
|
|
2292
2283
|
}),
|
|
2293
2284
|
});
|
|
2294
|
-
store.markTaskShared(entry.taskId, entry.hubTaskId, chunks.length, entry.visibility, entry.groupId);
|
|
2285
|
+
store.markTaskShared(entry.taskId, entry.hubTaskId, chunks.length, entry.visibility, entry.groupId, currentHubInstanceId);
|
|
2295
2286
|
} catch (err) {
|
|
2296
2287
|
ctx.log.warn(`incremental sync failed for task=${entry.taskId}: ${err}`);
|
|
2297
2288
|
}
|