@memtensor/memos-local-openclaw-plugin 1.0.4-beta.6 → 1.0.4-beta.7
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/README.md +39 -22
- package/dist/capture/index.d.ts +1 -1
- package/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +27 -7
- package/dist/capture/index.js.map +1 -1
- package/dist/client/connector.d.ts +29 -0
- package/dist/client/connector.d.ts.map +1 -0
- package/dist/client/connector.js +218 -0
- package/dist/client/connector.js.map +1 -0
- package/dist/client/hub.d.ts +61 -0
- package/dist/client/hub.d.ts.map +1 -0
- package/dist/client/hub.js +170 -0
- package/dist/client/hub.js.map +1 -0
- package/dist/client/skill-sync.d.ts +36 -0
- package/dist/client/skill-sync.d.ts.map +1 -0
- package/dist/client/skill-sync.js +226 -0
- package/dist/client/skill-sync.js.map +1 -0
- package/dist/config.d.ts +2 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +72 -3
- package/dist/config.js.map +1 -1
- package/dist/embedding/index.d.ts +4 -2
- package/dist/embedding/index.d.ts.map +1 -1
- package/dist/embedding/index.js +17 -1
- package/dist/embedding/index.js.map +1 -1
- package/dist/hub/auth.d.ts +19 -0
- package/dist/hub/auth.d.ts.map +1 -0
- package/dist/hub/auth.js +70 -0
- package/dist/hub/auth.js.map +1 -0
- package/dist/hub/server.d.ts +41 -0
- package/dist/hub/server.d.ts.map +1 -0
- package/dist/hub/server.js +767 -0
- package/dist/hub/server.js.map +1 -0
- package/dist/hub/user-manager.d.ts +31 -0
- package/dist/hub/user-manager.d.ts.map +1 -0
- package/dist/hub/user-manager.js +129 -0
- package/dist/hub/user-manager.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -4
- package/dist/index.js.map +1 -1
- package/dist/ingest/providers/index.d.ts +10 -2
- package/dist/ingest/providers/index.d.ts.map +1 -1
- package/dist/ingest/providers/index.js +209 -43
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/ingest/providers/openai.d.ts +1 -0
- package/dist/ingest/providers/openai.d.ts.map +1 -1
- package/dist/ingest/providers/openai.js +1 -0
- package/dist/ingest/providers/openai.js.map +1 -1
- package/dist/ingest/task-processor.js +1 -1
- package/dist/ingest/task-processor.js.map +1 -1
- package/dist/openclaw-api.d.ts +53 -0
- package/dist/openclaw-api.d.ts.map +1 -0
- package/dist/openclaw-api.js +189 -0
- package/dist/openclaw-api.js.map +1 -0
- package/dist/recall/engine.js +1 -1
- package/dist/recall/engine.js.map +1 -1
- package/dist/shared/llm-call.d.ts +4 -2
- package/dist/shared/llm-call.d.ts.map +1 -1
- package/dist/shared/llm-call.js +20 -81
- package/dist/shared/llm-call.js.map +1 -1
- package/dist/sharing/types.contract.d.ts +2 -0
- package/dist/sharing/types.contract.d.ts.map +1 -0
- package/dist/sharing/types.contract.js +3 -0
- package/dist/sharing/types.contract.js.map +1 -0
- package/dist/sharing/types.d.ts +80 -0
- package/dist/sharing/types.d.ts.map +1 -0
- package/dist/sharing/types.js +3 -0
- package/dist/sharing/types.js.map +1 -0
- package/dist/skill/evaluator.d.ts.map +1 -1
- package/dist/skill/evaluator.js +2 -2
- package/dist/skill/evaluator.js.map +1 -1
- package/dist/skill/evolver.d.ts +0 -2
- package/dist/skill/evolver.d.ts.map +1 -1
- package/dist/skill/evolver.js +0 -3
- package/dist/skill/evolver.js.map +1 -1
- package/dist/skill/generator.d.ts.map +1 -1
- package/dist/skill/generator.js +4 -4
- package/dist/skill/generator.js.map +1 -1
- package/dist/skill/upgrader.js +1 -1
- package/dist/skill/upgrader.js.map +1 -1
- package/dist/skill/validator.js +1 -1
- package/dist/skill/validator.js.map +1 -1
- package/dist/storage/ensure-binding.d.ts.map +1 -1
- package/dist/storage/ensure-binding.js +3 -1
- package/dist/storage/ensure-binding.js.map +1 -1
- package/dist/storage/sqlite.d.ts +329 -1
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +909 -4
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/telemetry.d.ts +5 -12
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +38 -135
- package/dist/telemetry.js.map +1 -1
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/memory-search.d.ts +5 -2
- package/dist/tools/memory-search.d.ts.map +1 -1
- package/dist/tools/memory-search.js +50 -7
- package/dist/tools/memory-search.js.map +1 -1
- package/dist/tools/network-memory-detail.d.ts +4 -0
- package/dist/tools/network-memory-detail.d.ts.map +1 -0
- package/dist/tools/network-memory-detail.js +34 -0
- package/dist/tools/network-memory-detail.js.map +1 -0
- package/dist/types.d.ts +49 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +3965 -459
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +51 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +1564 -23
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +769 -67
- package/openclaw.plugin.json +2 -1
- package/package.json +4 -3
- package/scripts/postinstall.cjs +283 -46
- package/skill/memos-memory-guide/SKILL.md +82 -20
- package/src/capture/index.ts +27 -7
- package/src/client/connector.ts +212 -0
- package/src/client/hub.ts +207 -0
- package/src/client/skill-sync.ts +216 -0
- package/src/config.ts +94 -3
- package/src/embedding/index.ts +21 -1
- package/src/hub/auth.ts +78 -0
- package/src/hub/server.ts +754 -0
- package/src/hub/user-manager.ts +143 -0
- package/src/index.ts +13 -5
- package/src/ingest/providers/index.ts +246 -46
- package/src/ingest/providers/openai.ts +1 -1
- package/src/ingest/task-processor.ts +1 -1
- package/src/openclaw-api.ts +287 -0
- package/src/recall/engine.ts +1 -1
- package/src/shared/llm-call.ts +23 -95
- package/src/sharing/types.contract.ts +40 -0
- package/src/sharing/types.ts +102 -0
- package/src/skill/evaluator.ts +3 -2
- package/src/skill/evolver.ts +0 -5
- package/src/skill/generator.ts +6 -4
- package/src/skill/upgrader.ts +1 -1
- package/src/skill/validator.ts +1 -1
- package/src/storage/ensure-binding.ts +3 -1
- package/src/storage/sqlite.ts +1159 -4
- package/src/telemetry.ts +39 -152
- package/src/tools/index.ts +1 -0
- package/src/tools/memory-search.ts +58 -8
- package/src/tools/network-memory-detail.ts +34 -0
- package/src/types.ts +44 -2
- package/src/viewer/html.ts +3965 -459
- package/src/viewer/server.ts +1452 -25
package/index.ts
CHANGED
|
@@ -9,8 +9,8 @@ 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 { fileURLToPath } from "url";
|
|
13
12
|
import { buildContext } from "./src/config";
|
|
13
|
+
import type { HostModelsConfig } from "./src/openclaw-api";
|
|
14
14
|
import { ensureSqliteBinding } from "./src/storage/ensure-binding";
|
|
15
15
|
import { SqliteStore } from "./src/storage/sqlite";
|
|
16
16
|
import { Embedder } from "./src/embedding";
|
|
@@ -19,6 +19,10 @@ import { RecallEngine } from "./src/recall/engine";
|
|
|
19
19
|
import { captureMessages, stripInboundMetadata } from "./src/capture";
|
|
20
20
|
import { DEFAULTS } from "./src/types";
|
|
21
21
|
import { ViewerServer } from "./src/viewer/server";
|
|
22
|
+
import { HubServer } from "./src/hub/server";
|
|
23
|
+
import { hubGetMemoryDetail, hubRequestJson, hubSearchMemories, hubSearchSkills, resolveHubClient } from "./src/client/hub";
|
|
24
|
+
import { getHubStatus, connectToHub } from "./src/client/connector";
|
|
25
|
+
import { fetchHubSkillBundle, publishSkillBundleToHub, restoreSkillBundleFromHub, unpublishSkillBundleFromHub } from "./src/client/skill-sync";
|
|
22
26
|
import { SkillEvolver } from "./src/skill/evolver";
|
|
23
27
|
import { SkillInstaller } from "./src/skill/installer";
|
|
24
28
|
import { Summarizer } from "./src/ingest/providers";
|
|
@@ -78,20 +82,13 @@ const memosLocalPlugin = {
|
|
|
78
82
|
|
|
79
83
|
register(api: OpenClawPluginApi) {
|
|
80
84
|
// ─── Ensure better-sqlite3 native module is available ───
|
|
81
|
-
const pluginDir = path.dirname(
|
|
82
|
-
|
|
83
|
-
function normalizeFsPath(p: string): string {
|
|
84
|
-
return path.resolve(p).replace(/\\/g, "/").toLowerCase();
|
|
85
|
-
}
|
|
86
|
-
|
|
85
|
+
const pluginDir = path.dirname(new URL(import.meta.url).pathname);
|
|
87
86
|
let sqliteReady = false;
|
|
88
87
|
|
|
89
88
|
function trySqliteLoad(): boolean {
|
|
90
89
|
try {
|
|
91
90
|
const resolved = require.resolve("better-sqlite3", { paths: [pluginDir] });
|
|
92
|
-
|
|
93
|
-
const pluginNorm = normalizeFsPath(pluginDir);
|
|
94
|
-
if (!resolvedNorm.startsWith(pluginNorm + "/") && resolvedNorm !== pluginNorm) {
|
|
91
|
+
if (!resolved.startsWith(pluginDir)) {
|
|
95
92
|
api.logger.warn(`memos-local: better-sqlite3 resolved outside plugin dir: ${resolved}`);
|
|
96
93
|
return false;
|
|
97
94
|
}
|
|
@@ -173,19 +170,37 @@ const memosLocalPlugin = {
|
|
|
173
170
|
}
|
|
174
171
|
}
|
|
175
172
|
|
|
176
|
-
|
|
173
|
+
let pluginCfg = (api.pluginConfig ?? {}) as Record<string, unknown>;
|
|
177
174
|
const stateDir = api.resolvePath("~/.openclaw");
|
|
175
|
+
|
|
176
|
+
// Fallback: read config from file if not provided by OpenClaw
|
|
177
|
+
const configPath = path.join(stateDir, "state", "memos-local", "config.json");
|
|
178
|
+
if (Object.keys(pluginCfg).length === 0 && fs.existsSync(configPath)) {
|
|
179
|
+
try {
|
|
180
|
+
const fileConfig = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
181
|
+
pluginCfg = fileConfig;
|
|
182
|
+
api.logger.info(`memos-local: loaded config from ${configPath}`);
|
|
183
|
+
} catch (e) {
|
|
184
|
+
api.logger.warn(`memos-local: failed to load config from ${configPath}: ${e}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Extract host model providers so OpenClawAPIClient can proxy completion/embedding
|
|
189
|
+
const hostModels: HostModelsConfig | undefined = api.config?.models?.providers
|
|
190
|
+
? { providers: api.config.models.providers as Record<string, import("./src/openclaw-api").HostModelProvider> }
|
|
191
|
+
: undefined;
|
|
192
|
+
|
|
178
193
|
const ctx = buildContext(stateDir, process.cwd(), pluginCfg as any, {
|
|
179
194
|
debug: (msg: string) => api.logger.info(`[debug] ${msg}`),
|
|
180
195
|
info: (msg: string) => api.logger.info(msg),
|
|
181
196
|
warn: (msg: string) => api.logger.warn(msg),
|
|
182
197
|
error: (msg: string) => api.logger.warn(`[error] ${msg}`),
|
|
183
|
-
});
|
|
198
|
+
}, hostModels);
|
|
184
199
|
|
|
185
200
|
ensureSqliteBinding(ctx.log);
|
|
186
201
|
|
|
187
202
|
const store = new SqliteStore(ctx.config.storage!.dbPath!, ctx.log);
|
|
188
|
-
const embedder = new Embedder(ctx.config.embedding, ctx.log);
|
|
203
|
+
const embedder = new Embedder(ctx.config.embedding, ctx.log, ctx.openclawAPI);
|
|
189
204
|
const worker = new IngestWorker(store, embedder, ctx);
|
|
190
205
|
const engine = new RecallEngine(store, embedder, ctx);
|
|
191
206
|
const evidenceTag = ctx.config.capture?.evidenceWrapperTag ?? DEFAULTS.evidenceWrapperTag;
|
|
@@ -193,7 +208,6 @@ const memosLocalPlugin = {
|
|
|
193
208
|
const workspaceDir = api.resolvePath("~/.openclaw/workspace");
|
|
194
209
|
const skillCtx = { ...ctx, workspaceDir };
|
|
195
210
|
const skillEvolver = new SkillEvolver(store, engine, skillCtx);
|
|
196
|
-
skillEvolver.onSkillEvolved = (name, type) => telemetry.trackSkillEvolved(name, type);
|
|
197
211
|
const skillInstaller = new SkillInstaller(store, skillCtx);
|
|
198
212
|
|
|
199
213
|
let pluginVersion = "0.0.0";
|
|
@@ -250,7 +264,7 @@ const memosLocalPlugin = {
|
|
|
250
264
|
});
|
|
251
265
|
});
|
|
252
266
|
|
|
253
|
-
const summarizer = new Summarizer(ctx.config.summarizer, ctx.log);
|
|
267
|
+
const summarizer = new Summarizer(ctx.config.summarizer, ctx.log, ctx.openclawAPI);
|
|
254
268
|
|
|
255
269
|
api.logger.info(`memos-local: initialized (db: ${ctx.config.storage!.dbPath})`);
|
|
256
270
|
|
|
@@ -258,18 +272,6 @@ const memosLocalPlugin = {
|
|
|
258
272
|
// Falls back to "main" when no hook has fired yet (single-agent setups).
|
|
259
273
|
let currentAgentId = "main";
|
|
260
274
|
|
|
261
|
-
// ─── Check allowPromptInjection policy ───
|
|
262
|
-
// When allowPromptInjection=false, the prompt mutation fields (such as prependContext) in the hook return value
|
|
263
|
-
// will be stripped by the framework. Skip auto-recall to avoid unnecessary LLM/embedding calls.
|
|
264
|
-
const pluginEntry = (api.config as any)?.plugins?.entries?.[api.id];
|
|
265
|
-
const allowPromptInjection = pluginEntry?.hooks?.allowPromptInjection !== false;
|
|
266
|
-
if (!allowPromptInjection) {
|
|
267
|
-
api.logger.info("memos-local: allowPromptInjection=false, auto-recall disabled");
|
|
268
|
-
}
|
|
269
|
-
else {
|
|
270
|
-
api.logger.info("memos-local: allowPromptInjection=true, auto-recall enabled");
|
|
271
|
-
}
|
|
272
|
-
|
|
273
275
|
const trackTool = (toolName: string, fn: (...args: any[]) => Promise<any>) =>
|
|
274
276
|
async (...args: any[]) => {
|
|
275
277
|
const t0 = performance.now();
|
|
@@ -281,7 +283,6 @@ const memosLocalPlugin = {
|
|
|
281
283
|
return result;
|
|
282
284
|
} catch (e) {
|
|
283
285
|
ok = false;
|
|
284
|
-
telemetry.trackError(toolName, (e as Error)?.name ?? "unknown");
|
|
285
286
|
throw e;
|
|
286
287
|
} finally {
|
|
287
288
|
const dur = performance.now() - t0;
|
|
@@ -303,6 +304,89 @@ const memosLocalPlugin = {
|
|
|
303
304
|
}
|
|
304
305
|
};
|
|
305
306
|
|
|
307
|
+
const getCurrentOwner = () => `agent:${currentAgentId}`;
|
|
308
|
+
const resolveMemorySearchScope = (scope?: string): "local" | "group" | "all" =>
|
|
309
|
+
scope === "group" || scope === "all" ? scope : "local";
|
|
310
|
+
const resolveMemoryShareTarget = (target?: string): "agents" | "hub" | "both" =>
|
|
311
|
+
target === "hub" || target === "both" ? target : "agents";
|
|
312
|
+
const resolveMemoryUnshareTarget = (target?: string): "agents" | "hub" | "all" =>
|
|
313
|
+
target === "agents" || target === "hub" ? target : "all";
|
|
314
|
+
const resolveSkillPublishTarget = (target?: string, scope?: string): "agents" | "hub" => {
|
|
315
|
+
if (target === "hub") return "hub";
|
|
316
|
+
if (target === "agents") return "agents";
|
|
317
|
+
return scope === "public" || scope === "group" ? "hub" : "agents";
|
|
318
|
+
};
|
|
319
|
+
const resolveSkillHubVisibility = (visibility?: string, scope?: string): "public" | "group" =>
|
|
320
|
+
visibility === "group" || scope === "group" ? "group" : "public";
|
|
321
|
+
const resolveSkillUnpublishTarget = (target?: string): "agents" | "hub" | "all" =>
|
|
322
|
+
target === "hub" || target === "all" ? target : "agents";
|
|
323
|
+
|
|
324
|
+
const shareMemoryToHub = async (
|
|
325
|
+
chunkId: string,
|
|
326
|
+
input?: { visibility?: "public" | "group"; groupId?: string; hubAddress?: string; userToken?: string },
|
|
327
|
+
): Promise<{ memoryId: string; visibility: "public" | "group"; groupId: string | null }> => {
|
|
328
|
+
const chunk = store.getChunk(chunkId);
|
|
329
|
+
if (!chunk) {
|
|
330
|
+
throw new Error(`Memory not found: ${chunkId}`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const visibility = input?.visibility === "group" ? "group" : "public";
|
|
334
|
+
const groupId = visibility === "group" ? (input?.groupId ?? null) : null;
|
|
335
|
+
const hubClient = await resolveHubClient(store, ctx, { hubAddress: input?.hubAddress, userToken: input?.userToken });
|
|
336
|
+
const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/share", {
|
|
337
|
+
method: "POST",
|
|
338
|
+
body: JSON.stringify({
|
|
339
|
+
memory: {
|
|
340
|
+
sourceChunkId: chunk.id,
|
|
341
|
+
role: chunk.role,
|
|
342
|
+
content: chunk.content,
|
|
343
|
+
summary: chunk.summary,
|
|
344
|
+
kind: chunk.kind,
|
|
345
|
+
groupId,
|
|
346
|
+
visibility,
|
|
347
|
+
},
|
|
348
|
+
}),
|
|
349
|
+
}) as { memoryId?: string; visibility?: "public" | "group" };
|
|
350
|
+
|
|
351
|
+
const now = Date.now();
|
|
352
|
+
const existing = store.getHubMemoryBySource(hubClient.userId, chunk.id);
|
|
353
|
+
store.upsertHubMemory({
|
|
354
|
+
id: response?.memoryId ?? existing?.id ?? `${chunk.id}-hub`,
|
|
355
|
+
sourceChunkId: chunk.id,
|
|
356
|
+
sourceUserId: hubClient.userId,
|
|
357
|
+
role: chunk.role,
|
|
358
|
+
content: chunk.content,
|
|
359
|
+
summary: chunk.summary ?? "",
|
|
360
|
+
kind: chunk.kind,
|
|
361
|
+
groupId,
|
|
362
|
+
visibility,
|
|
363
|
+
createdAt: existing?.createdAt ?? now,
|
|
364
|
+
updatedAt: now,
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
memoryId: response?.memoryId ?? existing?.id ?? `${chunk.id}-hub`,
|
|
369
|
+
visibility,
|
|
370
|
+
groupId,
|
|
371
|
+
};
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const unshareMemoryFromHub = async (
|
|
375
|
+
chunkId: string,
|
|
376
|
+
input?: { hubAddress?: string; userToken?: string },
|
|
377
|
+
): Promise<void> => {
|
|
378
|
+
const chunk = store.getChunk(chunkId);
|
|
379
|
+
if (!chunk) {
|
|
380
|
+
throw new Error(`Memory not found: ${chunkId}`);
|
|
381
|
+
}
|
|
382
|
+
const hubClient = await resolveHubClient(store, ctx, { hubAddress: input?.hubAddress, userToken: input?.userToken });
|
|
383
|
+
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/unshare", {
|
|
384
|
+
method: "POST",
|
|
385
|
+
body: JSON.stringify({ sourceChunkId: chunk.id }),
|
|
386
|
+
});
|
|
387
|
+
store.deleteHubMemoryBySource(hubClient.userId, chunk.id);
|
|
388
|
+
};
|
|
389
|
+
|
|
306
390
|
// ─── Tool: memory_search ───
|
|
307
391
|
|
|
308
392
|
api.registerTool(
|
|
@@ -311,20 +395,43 @@ const memosLocalPlugin = {
|
|
|
311
395
|
label: "Memory Search",
|
|
312
396
|
description:
|
|
313
397
|
"Search long-term conversation memory for past conversations, user preferences, decisions, and experiences. " +
|
|
314
|
-
"
|
|
315
|
-
"
|
|
316
|
-
"Pass only a short natural-language query (2-5 key words).",
|
|
398
|
+
"Use scope='local' for this agent plus local shared memories, or scope='group'/'all' to include Hub-shared memories. " +
|
|
399
|
+
"Supports optional maxResults, minScore, and role filtering when you need tighter control.",
|
|
317
400
|
parameters: Type.Object({
|
|
318
401
|
query: Type.String({ description: "Short natural language search query (2-5 key words)" }),
|
|
402
|
+
scope: Type.Optional(Type.String({ description: "Search scope: 'local' (default), 'group', or 'all'. Use group/all to include Hub-shared memories." })),
|
|
403
|
+
maxResults: Type.Optional(Type.Number({ description: "Maximum results to return. Default 10, max 20." })),
|
|
404
|
+
minScore: Type.Optional(Type.Number({ description: "Minimum score threshold for local recall. Default 0.45, floor 0.35." })),
|
|
405
|
+
role: Type.Optional(Type.String({ description: "Optional local role filter: 'user', 'assistant', 'tool', or 'system'." })),
|
|
406
|
+
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for group/all search." })),
|
|
407
|
+
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for group/all search." })),
|
|
319
408
|
}),
|
|
320
409
|
execute: trackTool("memory_search", async (_toolCallId: any, params: any) => {
|
|
321
|
-
const {
|
|
322
|
-
|
|
323
|
-
|
|
410
|
+
const {
|
|
411
|
+
query,
|
|
412
|
+
scope: rawScope,
|
|
413
|
+
maxResults,
|
|
414
|
+
minScore: rawMinScore,
|
|
415
|
+
role: rawRole,
|
|
416
|
+
hubAddress,
|
|
417
|
+
userToken,
|
|
418
|
+
} = params as {
|
|
419
|
+
query: string;
|
|
420
|
+
scope?: string;
|
|
421
|
+
maxResults?: number;
|
|
422
|
+
minScore?: number;
|
|
423
|
+
role?: string;
|
|
424
|
+
hubAddress?: string;
|
|
425
|
+
userToken?: string;
|
|
426
|
+
};
|
|
427
|
+
const role = rawRole === "user" || rawRole === "assistant" || rawRole === "tool" || rawRole === "system" ? rawRole : undefined;
|
|
428
|
+
const minScore = typeof rawMinScore === "number" ? Math.max(0.35, Math.min(1, rawMinScore)) : undefined;
|
|
429
|
+
const searchScope = resolveMemorySearchScope(rawScope);
|
|
430
|
+
const searchLimit = typeof maxResults === "number" ? Math.max(1, Math.min(20, Math.round(maxResults))) : 10;
|
|
324
431
|
|
|
325
432
|
const agentId = currentAgentId;
|
|
326
|
-
const ownerFilter = [
|
|
327
|
-
const effectiveMaxResults =
|
|
433
|
+
const ownerFilter = [getCurrentOwner(), "public"];
|
|
434
|
+
const effectiveMaxResults = searchLimit;
|
|
328
435
|
ctx.log.debug(`memory_search query="${query}" maxResults=${effectiveMaxResults} minScore=${minScore ?? 0.45} role=${role ?? "all"} owner=agent:${agentId}`);
|
|
329
436
|
const result = await engine.search({ query, maxResults: effectiveMaxResults, minScore, role, ownerFilter });
|
|
330
437
|
ctx.log.debug(`memory_search raw candidates: ${result.hits.length}`);
|
|
@@ -337,14 +444,13 @@ const memosLocalPlugin = {
|
|
|
337
444
|
original_excerpt: (h.original_excerpt ?? "").slice(0, 200),
|
|
338
445
|
}));
|
|
339
446
|
|
|
340
|
-
if (result.hits.length === 0) {
|
|
447
|
+
if (result.hits.length === 0 && searchScope === "local") {
|
|
341
448
|
return {
|
|
342
449
|
content: [{ type: "text", text: result.meta.note ?? "No relevant memories found." }],
|
|
343
450
|
details: { candidates: [], meta: result.meta },
|
|
344
451
|
};
|
|
345
452
|
}
|
|
346
453
|
|
|
347
|
-
// LLM relevance + sufficiency filtering
|
|
348
454
|
let filteredHits = result.hits;
|
|
349
455
|
let sufficient = false;
|
|
350
456
|
|
|
@@ -362,14 +468,61 @@ const memosLocalPlugin = {
|
|
|
362
468
|
const indexSet = new Set(filterResult.relevant);
|
|
363
469
|
filteredHits = result.hits.filter((_, i) => indexSet.has(i + 1));
|
|
364
470
|
ctx.log.debug(`memory_search LLM filter: ${result.hits.length} → ${filteredHits.length} hits, sufficient=${sufficient}`);
|
|
365
|
-
} else {
|
|
471
|
+
} else if (searchScope === "local") {
|
|
366
472
|
return {
|
|
367
473
|
content: [{ type: "text", text: "No relevant memories found for this query." }],
|
|
368
474
|
details: { candidates: rawCandidates, filtered: [], meta: result.meta },
|
|
369
475
|
};
|
|
476
|
+
} else {
|
|
477
|
+
filteredHits = [];
|
|
370
478
|
}
|
|
371
479
|
}
|
|
372
480
|
|
|
481
|
+
const beforeDedup = filteredHits.length;
|
|
482
|
+
filteredHits = deduplicateHits(filteredHits);
|
|
483
|
+
ctx.log.debug(`memory_search dedup: ${beforeDedup} → ${filteredHits.length}`);
|
|
484
|
+
|
|
485
|
+
const localDetailsHits = filteredHits.map((h) => {
|
|
486
|
+
let effectiveTaskId = h.taskId;
|
|
487
|
+
if (effectiveTaskId) {
|
|
488
|
+
const t = store.getTask(effectiveTaskId);
|
|
489
|
+
if (t && t.status === "skipped") effectiveTaskId = null;
|
|
490
|
+
}
|
|
491
|
+
return {
|
|
492
|
+
ref: h.ref,
|
|
493
|
+
chunkId: h.ref.chunkId,
|
|
494
|
+
taskId: effectiveTaskId,
|
|
495
|
+
skillId: h.skillId,
|
|
496
|
+
role: h.source.role,
|
|
497
|
+
score: h.score,
|
|
498
|
+
summary: h.summary,
|
|
499
|
+
};
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
if (searchScope !== "local") {
|
|
503
|
+
const hub = await hubSearchMemories(store, ctx, { query, maxResults: searchLimit, scope: searchScope as any, hubAddress, userToken }).catch(() => ({ hits: [], meta: { totalCandidates: 0, searchedGroups: [], includedPublic: searchScope === "all" } }));
|
|
504
|
+
const localText = filteredHits.length > 0
|
|
505
|
+
? filteredHits.map((h, i) => {
|
|
506
|
+
const excerpt = h.original_excerpt.length > 220 ? h.original_excerpt.slice(0, 217) + "..." : h.original_excerpt;
|
|
507
|
+
return `${i + 1}. [${h.source.role}] ${excerpt}`;
|
|
508
|
+
}).join("\n")
|
|
509
|
+
: "(none)";
|
|
510
|
+
const hubText = hub.hits.length > 0
|
|
511
|
+
? hub.hits.map((h, i) => `${i + 1}. [${h.ownerName}] ${h.summary}${h.groupName ? ` (${h.groupName})` : ""}`).join("\n")
|
|
512
|
+
: "(none)";
|
|
513
|
+
|
|
514
|
+
return {
|
|
515
|
+
content: [{
|
|
516
|
+
type: "text",
|
|
517
|
+
text: `Local results:\n${localText}\n\nHub results:\n${hubText}`,
|
|
518
|
+
}],
|
|
519
|
+
details: {
|
|
520
|
+
local: { hits: localDetailsHits, meta: result.meta },
|
|
521
|
+
hub,
|
|
522
|
+
},
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
373
526
|
if (filteredHits.length === 0) {
|
|
374
527
|
return {
|
|
375
528
|
content: [{ type: "text", text: "No relevant memories found for this query." }],
|
|
@@ -377,10 +530,6 @@ const memosLocalPlugin = {
|
|
|
377
530
|
};
|
|
378
531
|
}
|
|
379
532
|
|
|
380
|
-
const beforeDedup = filteredHits.length;
|
|
381
|
-
filteredHits = deduplicateHits(filteredHits);
|
|
382
|
-
ctx.log.debug(`memory_search dedup: ${beforeDedup} → ${filteredHits.length}`);
|
|
383
|
-
|
|
384
533
|
const lines = filteredHits.map((h, i) => {
|
|
385
534
|
const excerpt = h.original_excerpt;
|
|
386
535
|
const parts = [`${i + 1}. [${h.source.role}]`];
|
|
@@ -473,7 +622,7 @@ const memosLocalPlugin = {
|
|
|
473
622
|
if (!anchorChunk) {
|
|
474
623
|
return {
|
|
475
624
|
content: [{ type: "text", text: `Chunk not found: ${chunkId}` }],
|
|
476
|
-
details: { error: "not_found" },
|
|
625
|
+
details: { error: "not_found", entries: [] },
|
|
477
626
|
};
|
|
478
627
|
}
|
|
479
628
|
|
|
@@ -522,7 +671,7 @@ const memosLocalPlugin = {
|
|
|
522
671
|
Type.Number({ description: `Max chars (default ${DEFAULTS.getMaxCharsDefault}, max ${DEFAULTS.getMaxCharsMax})` }),
|
|
523
672
|
),
|
|
524
673
|
}),
|
|
525
|
-
execute: trackTool("memory_get", async (_toolCallId: any, params: any) => {
|
|
674
|
+
execute: trackTool("memory_get", async (_toolCallId: any, params: any, context?: any) => {
|
|
526
675
|
const { chunkId, maxChars } = params as { chunkId: string; maxChars?: number };
|
|
527
676
|
const limit = Math.min(maxChars ?? DEFAULTS.getMaxCharsDefault, DEFAULTS.getMaxCharsMax);
|
|
528
677
|
|
|
@@ -629,6 +778,209 @@ const memosLocalPlugin = {
|
|
|
629
778
|
{ name: "task_summary" },
|
|
630
779
|
);
|
|
631
780
|
|
|
781
|
+
// ─── Tool: task_share ───
|
|
782
|
+
|
|
783
|
+
api.registerTool(
|
|
784
|
+
{
|
|
785
|
+
name: "task_share",
|
|
786
|
+
label: "Task Share",
|
|
787
|
+
description:
|
|
788
|
+
"Share one existing local task and its chunks to the configured hub. " +
|
|
789
|
+
"Minimal MVP path for validating team task sharing.",
|
|
790
|
+
parameters: Type.Object({
|
|
791
|
+
taskId: Type.String({ description: "Local task ID to share" }),
|
|
792
|
+
visibility: Type.Optional(Type.String({ description: "Share visibility: 'public' (default) or 'group'" })),
|
|
793
|
+
groupId: Type.Optional(Type.String({ description: "Optional group ID when visibility='group'" })),
|
|
794
|
+
}),
|
|
795
|
+
execute: trackTool("task_share", async (_toolCallId: any, params: any) => {
|
|
796
|
+
const { taskId, visibility: rawVisibility, groupId } = params as {
|
|
797
|
+
taskId: string;
|
|
798
|
+
visibility?: string;
|
|
799
|
+
groupId?: string;
|
|
800
|
+
};
|
|
801
|
+
|
|
802
|
+
const task = store.getTask(taskId);
|
|
803
|
+
if (!task) {
|
|
804
|
+
return {
|
|
805
|
+
content: [{ type: "text", text: `Task not found: ${taskId}` }],
|
|
806
|
+
details: { error: "not_found", taskId },
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const chunks = store.getChunksByTask(taskId);
|
|
811
|
+
if (chunks.length === 0) {
|
|
812
|
+
return {
|
|
813
|
+
content: [{ type: "text", text: `Task ${taskId} has no chunks to share.` }],
|
|
814
|
+
details: { error: "no_chunks", taskId },
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const visibility = rawVisibility === "group" ? "group" : "public";
|
|
819
|
+
const hubClient = await resolveHubClient(store, ctx);
|
|
820
|
+
const { v4: uuidv4 } = require("uuid");
|
|
821
|
+
const hubTaskId = uuidv4();
|
|
822
|
+
|
|
823
|
+
const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/share", {
|
|
824
|
+
method: "POST",
|
|
825
|
+
body: JSON.stringify({
|
|
826
|
+
task: {
|
|
827
|
+
id: hubTaskId,
|
|
828
|
+
sourceTaskId: task.id,
|
|
829
|
+
sourceUserId: hubClient.userId,
|
|
830
|
+
title: task.title,
|
|
831
|
+
summary: task.summary,
|
|
832
|
+
groupId: visibility === "group" ? (groupId ?? null) : null,
|
|
833
|
+
visibility,
|
|
834
|
+
createdAt: task.startedAt,
|
|
835
|
+
updatedAt: task.updatedAt,
|
|
836
|
+
},
|
|
837
|
+
chunks: chunks.map((chunk) => ({
|
|
838
|
+
id: uuidv4(),
|
|
839
|
+
hubTaskId,
|
|
840
|
+
sourceTaskId: task.id,
|
|
841
|
+
sourceChunkId: chunk.id,
|
|
842
|
+
sourceUserId: hubClient.userId,
|
|
843
|
+
role: chunk.role,
|
|
844
|
+
content: chunk.content,
|
|
845
|
+
summary: chunk.summary,
|
|
846
|
+
kind: chunk.kind,
|
|
847
|
+
createdAt: chunk.createdAt,
|
|
848
|
+
})),
|
|
849
|
+
}),
|
|
850
|
+
}) as any;
|
|
851
|
+
|
|
852
|
+
store.markTaskShared(task.id, hubTaskId, chunks.length, visibility, groupId);
|
|
853
|
+
|
|
854
|
+
return {
|
|
855
|
+
content: [{ type: "text", text: `Shared task "${task.title}" with ${chunks.length} chunks to the hub.` }],
|
|
856
|
+
details: {
|
|
857
|
+
shared: true,
|
|
858
|
+
taskId: task.id,
|
|
859
|
+
visibility,
|
|
860
|
+
chunkCount: chunks.length,
|
|
861
|
+
hubUrl: hubClient.hubUrl,
|
|
862
|
+
response,
|
|
863
|
+
},
|
|
864
|
+
};
|
|
865
|
+
}),
|
|
866
|
+
},
|
|
867
|
+
{ name: "task_share" },
|
|
868
|
+
);
|
|
869
|
+
|
|
870
|
+
// ─── Tool: task_unshare ───
|
|
871
|
+
|
|
872
|
+
api.registerTool(
|
|
873
|
+
{
|
|
874
|
+
name: "task_unshare",
|
|
875
|
+
label: "Task Unshare",
|
|
876
|
+
description: "Remove one previously shared task from the configured hub.",
|
|
877
|
+
parameters: Type.Object({
|
|
878
|
+
taskId: Type.String({ description: "Local task ID to unshare" }),
|
|
879
|
+
}),
|
|
880
|
+
execute: trackTool("task_unshare", async (_toolCallId: any, params: any) => {
|
|
881
|
+
const { taskId } = params as { taskId: string };
|
|
882
|
+
|
|
883
|
+
const task = store.getTask(taskId);
|
|
884
|
+
if (!task) {
|
|
885
|
+
return {
|
|
886
|
+
content: [{ type: "text", text: `Task not found: ${taskId}` }],
|
|
887
|
+
details: { error: "not_found", taskId },
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const hubClient = await resolveHubClient(store, ctx);
|
|
892
|
+
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/unshare", {
|
|
893
|
+
method: "POST",
|
|
894
|
+
body: JSON.stringify({
|
|
895
|
+
sourceUserId: hubClient.userId,
|
|
896
|
+
sourceTaskId: task.id,
|
|
897
|
+
}),
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
store.unmarkTaskShared(task.id);
|
|
901
|
+
|
|
902
|
+
return {
|
|
903
|
+
content: [{ type: "text", text: `Unshared task "${task.title}" from the hub.` }],
|
|
904
|
+
details: {
|
|
905
|
+
unshared: true,
|
|
906
|
+
taskId: task.id,
|
|
907
|
+
hubUrl: hubClient.hubUrl,
|
|
908
|
+
},
|
|
909
|
+
};
|
|
910
|
+
}),
|
|
911
|
+
},
|
|
912
|
+
{ name: "task_unshare" },
|
|
913
|
+
);
|
|
914
|
+
|
|
915
|
+
api.registerTool(
|
|
916
|
+
{
|
|
917
|
+
name: "network_memory_detail",
|
|
918
|
+
label: "Network Memory Detail",
|
|
919
|
+
description: "Fetch the full detail for a Hub search hit returned by memory_search(scope=group|all).",
|
|
920
|
+
parameters: Type.Object({
|
|
921
|
+
remoteHitId: Type.String({ description: "The remoteHitId returned by a Hub search hit" }),
|
|
922
|
+
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for tests or manual routing" })),
|
|
923
|
+
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for tests" })),
|
|
924
|
+
}),
|
|
925
|
+
execute: trackTool("network_memory_detail", async (_toolCallId: any, params: any) => {
|
|
926
|
+
const { remoteHitId, hubAddress, userToken } = params as {
|
|
927
|
+
remoteHitId: string;
|
|
928
|
+
hubAddress?: string;
|
|
929
|
+
userToken?: string;
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
const detail = await hubGetMemoryDetail(store, ctx, { remoteHitId, hubAddress, userToken });
|
|
933
|
+
return {
|
|
934
|
+
content: [{
|
|
935
|
+
type: "text",
|
|
936
|
+
text: `## Shared Memory Detail
|
|
937
|
+
|
|
938
|
+
${detail.summary}
|
|
939
|
+
|
|
940
|
+
${detail.content}`,
|
|
941
|
+
}],
|
|
942
|
+
details: detail,
|
|
943
|
+
};
|
|
944
|
+
}),
|
|
945
|
+
},
|
|
946
|
+
{ name: "network_memory_detail" },
|
|
947
|
+
);
|
|
948
|
+
|
|
949
|
+
api.registerTool(
|
|
950
|
+
{
|
|
951
|
+
name: "network_team_info",
|
|
952
|
+
label: "Network Team Info",
|
|
953
|
+
description:
|
|
954
|
+
"Show current Hub connection status, signed-in user, role, and group memberships. " +
|
|
955
|
+
"Use this as a preflight check before any Hub share/unshare or Hub pull operation.",
|
|
956
|
+
parameters: Type.Object({}),
|
|
957
|
+
execute: trackTool("network_team_info", async () => {
|
|
958
|
+
const status = await getHubStatus(store, ctx.config);
|
|
959
|
+
if (!status.connected || !status.user) {
|
|
960
|
+
return {
|
|
961
|
+
content: [{ type: "text", text: "Hub is not connected." }],
|
|
962
|
+
details: status,
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const groupNames = status.user.groups.map((group) => group.name);
|
|
967
|
+
return {
|
|
968
|
+
content: [{
|
|
969
|
+
type: "text",
|
|
970
|
+
text: `## Team Connection
|
|
971
|
+
|
|
972
|
+
User: ${status.user.username}
|
|
973
|
+
Role: ${status.user.role}
|
|
974
|
+
Hub: ${status.hubUrl ?? "(unknown)"}
|
|
975
|
+
Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
976
|
+
}],
|
|
977
|
+
details: status,
|
|
978
|
+
};
|
|
979
|
+
}),
|
|
980
|
+
},
|
|
981
|
+
{ name: "network_team_info" },
|
|
982
|
+
);
|
|
983
|
+
|
|
632
984
|
// ─── Tool: skill_get ───
|
|
633
985
|
|
|
634
986
|
api.registerTool(
|
|
@@ -745,7 +1097,6 @@ const memosLocalPlugin = {
|
|
|
745
1097
|
parameters: Type.Object({}),
|
|
746
1098
|
execute: trackTool("memory_viewer", async () => {
|
|
747
1099
|
ctx.log.debug(`memory_viewer called`);
|
|
748
|
-
telemetry.trackViewerOpened();
|
|
749
1100
|
const url = `http://127.0.0.1:${viewerPort}`;
|
|
750
1101
|
return {
|
|
751
1102
|
content: [
|
|
@@ -776,12 +1127,13 @@ const memosLocalPlugin = {
|
|
|
776
1127
|
api.registerTool(
|
|
777
1128
|
{
|
|
778
1129
|
name: "memory_write_public",
|
|
779
|
-
label: "Write
|
|
1130
|
+
label: "Write Local Shared Memory",
|
|
780
1131
|
description:
|
|
781
|
-
"Write a piece of information to
|
|
782
|
-
"Use this
|
|
1132
|
+
"Write a piece of information to local shared memory for all agents in this OpenClaw workspace. " +
|
|
1133
|
+
"Use this when you are creating a new shared note from scratch. This does not publish to Hub. " +
|
|
1134
|
+
"If you already have a memory chunk and want to expose it, use memory_share instead.",
|
|
783
1135
|
parameters: Type.Object({
|
|
784
|
-
content: Type.String({ description: "The content to write to
|
|
1136
|
+
content: Type.String({ description: "The content to write to local shared memory" }),
|
|
785
1137
|
summary: Type.Optional(Type.String({ description: "Optional short summary of the content" })),
|
|
786
1138
|
}),
|
|
787
1139
|
execute: trackTool("memory_write_public", async (_toolCallId: any, params: any) => {
|
|
@@ -826,7 +1178,7 @@ const memosLocalPlugin = {
|
|
|
826
1178
|
}
|
|
827
1179
|
|
|
828
1180
|
return {
|
|
829
|
-
content: [{ type: "text", text: `
|
|
1181
|
+
content: [{ type: "text", text: `Memory shared to local agents successfully (id: ${chunkId}).` }],
|
|
830
1182
|
details: { chunkId, owner: "public" },
|
|
831
1183
|
};
|
|
832
1184
|
}),
|
|
@@ -834,6 +1186,164 @@ const memosLocalPlugin = {
|
|
|
834
1186
|
{ name: "memory_write_public" },
|
|
835
1187
|
);
|
|
836
1188
|
|
|
1189
|
+
api.registerTool(
|
|
1190
|
+
{
|
|
1191
|
+
name: "memory_share",
|
|
1192
|
+
label: "Share Memory",
|
|
1193
|
+
description:
|
|
1194
|
+
"Share an existing memory either with local OpenClaw agents, to the Hub team, or to both targets. " +
|
|
1195
|
+
"Use this only for an existing chunkId. Use target='agents' for local multi-agent sharing, target='hub' for team sharing, or target='both' for both. " +
|
|
1196
|
+
"If you need to create a brand new shared memory instead of exposing an existing one, use memory_write_public.",
|
|
1197
|
+
parameters: Type.Object({
|
|
1198
|
+
chunkId: Type.String({ description: "Existing local memory chunk ID to share" }),
|
|
1199
|
+
target: Type.Optional(Type.String({ description: "Share target: 'agents' (default), 'hub', or 'both'" })),
|
|
1200
|
+
visibility: Type.Optional(Type.String({ description: "Hub visibility when target includes hub: 'public' (default) or 'group'" })),
|
|
1201
|
+
groupId: Type.Optional(Type.String({ description: "Optional Hub group ID when visibility='group'" })),
|
|
1202
|
+
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override" })),
|
|
1203
|
+
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override" })),
|
|
1204
|
+
}),
|
|
1205
|
+
execute: trackTool("memory_share", async (_toolCallId: any, params: any) => {
|
|
1206
|
+
const {
|
|
1207
|
+
chunkId,
|
|
1208
|
+
target: rawTarget,
|
|
1209
|
+
visibility: rawVisibility,
|
|
1210
|
+
groupId,
|
|
1211
|
+
hubAddress,
|
|
1212
|
+
userToken,
|
|
1213
|
+
} = params as {
|
|
1214
|
+
chunkId: string;
|
|
1215
|
+
target?: string;
|
|
1216
|
+
visibility?: string;
|
|
1217
|
+
groupId?: string;
|
|
1218
|
+
hubAddress?: string;
|
|
1219
|
+
userToken?: string;
|
|
1220
|
+
};
|
|
1221
|
+
|
|
1222
|
+
const chunk = store.getChunk(chunkId);
|
|
1223
|
+
if (!chunk) {
|
|
1224
|
+
return { content: [{ type: "text", text: `Memory not found: ${chunkId}` }], details: { error: "not_found", chunkId } };
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
const target = resolveMemoryShareTarget(rawTarget);
|
|
1228
|
+
const visibility = rawVisibility === "group" ? "group" : "public";
|
|
1229
|
+
const details: Record<string, unknown> = { chunkId, target };
|
|
1230
|
+
const messages: string[] = [];
|
|
1231
|
+
|
|
1232
|
+
if (target === "agents" || target === "both") {
|
|
1233
|
+
const local = store.markMemorySharedLocally(chunkId);
|
|
1234
|
+
if (!local.ok) {
|
|
1235
|
+
return { content: [{ type: "text", text: `Failed to share memory ${chunkId} to local agents.` }], details: { error: local.reason ?? "local_share_failed", chunkId, target } };
|
|
1236
|
+
}
|
|
1237
|
+
details.local = {
|
|
1238
|
+
shared: true,
|
|
1239
|
+
owner: local.owner,
|
|
1240
|
+
originalOwner: local.originalOwner ?? null,
|
|
1241
|
+
};
|
|
1242
|
+
messages.push("shared to local agents");
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
if (target === "hub" || target === "both") {
|
|
1246
|
+
const hub = await shareMemoryToHub(chunkId, { visibility, groupId, hubAddress, userToken });
|
|
1247
|
+
details.hub = {
|
|
1248
|
+
shared: true,
|
|
1249
|
+
memoryId: hub.memoryId,
|
|
1250
|
+
visibility: hub.visibility,
|
|
1251
|
+
groupId: hub.groupId,
|
|
1252
|
+
};
|
|
1253
|
+
messages.push(`shared to Hub (${hub.visibility})`);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
return {
|
|
1257
|
+
content: [{ type: "text", text: `Memory "${chunk.summary || chunk.id}" ${messages.join(" and ")}.` }],
|
|
1258
|
+
details,
|
|
1259
|
+
};
|
|
1260
|
+
}),
|
|
1261
|
+
},
|
|
1262
|
+
{ name: "memory_share" },
|
|
1263
|
+
);
|
|
1264
|
+
|
|
1265
|
+
api.registerTool(
|
|
1266
|
+
{
|
|
1267
|
+
name: "memory_unshare",
|
|
1268
|
+
label: "Unshare Memory",
|
|
1269
|
+
description:
|
|
1270
|
+
"Remove sharing from an existing memory. Use target='agents' to stop local multi-agent sharing, target='hub' to remove it from Hub, or target='all' (default) to remove both. " +
|
|
1271
|
+
"privateOwner is only needed for older public memories that were never tracked with an original owner.",
|
|
1272
|
+
parameters: Type.Object({
|
|
1273
|
+
chunkId: Type.String({ description: "Existing local memory chunk ID to unshare" }),
|
|
1274
|
+
target: Type.Optional(Type.String({ description: "Unshare target: 'agents', 'hub', or 'all' (default)" })),
|
|
1275
|
+
privateOwner: Type.Optional(Type.String({ description: "Optional owner to restore when converting a public memory back to private and no original owner was tracked" })),
|
|
1276
|
+
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override" })),
|
|
1277
|
+
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override" })),
|
|
1278
|
+
}),
|
|
1279
|
+
execute: trackTool("memory_unshare", async (_toolCallId: any, params: any) => {
|
|
1280
|
+
const {
|
|
1281
|
+
chunkId,
|
|
1282
|
+
target: rawTarget,
|
|
1283
|
+
privateOwner,
|
|
1284
|
+
hubAddress,
|
|
1285
|
+
userToken,
|
|
1286
|
+
} = params as {
|
|
1287
|
+
chunkId: string;
|
|
1288
|
+
target?: string;
|
|
1289
|
+
privateOwner?: string;
|
|
1290
|
+
hubAddress?: string;
|
|
1291
|
+
userToken?: string;
|
|
1292
|
+
};
|
|
1293
|
+
|
|
1294
|
+
const chunk = store.getChunk(chunkId);
|
|
1295
|
+
if (!chunk) {
|
|
1296
|
+
return { content: [{ type: "text", text: `Memory not found: ${chunkId}` }], details: { error: "not_found", chunkId } };
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
const target = resolveMemoryUnshareTarget(rawTarget);
|
|
1300
|
+
const details: Record<string, unknown> = { chunkId, target };
|
|
1301
|
+
const messages: string[] = [];
|
|
1302
|
+
|
|
1303
|
+
if (target === "agents" || target === "all") {
|
|
1304
|
+
const local = store.unmarkMemorySharedLocally(chunkId, privateOwner);
|
|
1305
|
+
if (!local.ok) {
|
|
1306
|
+
return {
|
|
1307
|
+
content: [{
|
|
1308
|
+
type: "text",
|
|
1309
|
+
text: local.reason === "original_owner_missing"
|
|
1310
|
+
? `Cannot restore memory "${chunk.summary || chunk.id}" to a private owner automatically. Pass privateOwner to unshare it locally.`
|
|
1311
|
+
: `Failed to stop local sharing for memory ${chunkId}.`,
|
|
1312
|
+
}],
|
|
1313
|
+
details: { error: local.reason ?? "local_unshare_failed", chunkId, target },
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
details.local = {
|
|
1317
|
+
shared: false,
|
|
1318
|
+
owner: local.owner,
|
|
1319
|
+
};
|
|
1320
|
+
messages.push("removed from local agent sharing");
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
if (target === "hub" || target === "all") {
|
|
1324
|
+
try {
|
|
1325
|
+
await unshareMemoryFromHub(chunkId, { hubAddress, userToken });
|
|
1326
|
+
details.hub = { shared: false };
|
|
1327
|
+
messages.push("removed from Hub");
|
|
1328
|
+
} catch (err) {
|
|
1329
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1330
|
+
if (target === "all" && msg.includes("hub client connection is not configured")) {
|
|
1331
|
+
details.hub = { shared: false, skipped: true, reason: "hub_not_configured" };
|
|
1332
|
+
} else {
|
|
1333
|
+
throw err;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
return {
|
|
1339
|
+
content: [{ type: "text", text: `Memory "${chunk.summary || chunk.id}" ${messages.join(" and ")}.` }],
|
|
1340
|
+
details,
|
|
1341
|
+
};
|
|
1342
|
+
}),
|
|
1343
|
+
},
|
|
1344
|
+
{ name: "memory_unshare" },
|
|
1345
|
+
);
|
|
1346
|
+
|
|
837
1347
|
// ─── Tool: skill_search ───
|
|
838
1348
|
|
|
839
1349
|
api.registerTool(
|
|
@@ -841,16 +1351,42 @@ const memosLocalPlugin = {
|
|
|
841
1351
|
name: "skill_search",
|
|
842
1352
|
label: "Skill Search",
|
|
843
1353
|
description:
|
|
844
|
-
"Search available skills by natural language.
|
|
845
|
-
"Use when you need a capability or guide and don't have a matching skill at hand.",
|
|
1354
|
+
"Search available skills by natural language. Use scope='mix' (default) for this agent plus local shared skills, 'self' for this agent only, 'public' for local shared skills only, or 'group'/'all' to include Hub skills as well. " +
|
|
1355
|
+
"Use this when you need a capability or guide and don't have a matching skill at hand.",
|
|
846
1356
|
parameters: Type.Object({
|
|
847
1357
|
query: Type.String({ description: "Natural language description of the needed skill" }),
|
|
848
|
-
scope: Type.Optional(Type.String({ description: "Search scope: 'mix' (default, self
|
|
1358
|
+
scope: Type.Optional(Type.String({ description: "Search scope: 'mix' (default), 'self', 'public', 'group', or 'all'." })),
|
|
849
1359
|
}),
|
|
850
|
-
execute: trackTool("skill_search", async (_toolCallId: any, params: any) => {
|
|
1360
|
+
execute: trackTool("skill_search", async (_toolCallId: any, params: any, context?: any) => {
|
|
851
1361
|
const { query: skillQuery, scope: rawScope } = params as { query: string; scope?: string };
|
|
852
1362
|
const scope = (rawScope === "self" || rawScope === "public") ? rawScope : "mix";
|
|
853
|
-
const currentOwner =
|
|
1363
|
+
const currentOwner = getCurrentOwner();
|
|
1364
|
+
|
|
1365
|
+
if (rawScope === "group" || rawScope === "all") {
|
|
1366
|
+
const [localHits, hub] = await Promise.all([
|
|
1367
|
+
engine.searchSkills(skillQuery, "mix" as any, currentOwner),
|
|
1368
|
+
hubSearchSkills(store, ctx, { query: skillQuery, maxResults: 10 }).catch(() => ({ hits: [] })),
|
|
1369
|
+
]);
|
|
1370
|
+
|
|
1371
|
+
if (localHits.length === 0 && hub.hits.length === 0) {
|
|
1372
|
+
return {
|
|
1373
|
+
content: [{ type: "text", text: `No relevant skills found for: "${skillQuery}" (scope: ${rawScope})` }],
|
|
1374
|
+
details: { query: skillQuery, scope: rawScope, local: { hits: [] }, hub },
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
const localText = localHits.length > 0
|
|
1379
|
+
? localHits.map((h, i) => `${i + 1}. [${h.name}] ${h.description.slice(0, 150)}${h.visibility === "public" ? " (shared to local agents)" : ""}`).join("\n")
|
|
1380
|
+
: "(none)";
|
|
1381
|
+
const hubText = hub.hits.length > 0
|
|
1382
|
+
? hub.hits.map((h, i) => `${i + 1}. [${h.name}] ${h.description.slice(0, 150)} (${h.visibility}${h.groupName ? `:${h.groupName}` : ""}, owner=${h.ownerName})`).join("\n")
|
|
1383
|
+
: "(none)";
|
|
1384
|
+
|
|
1385
|
+
return {
|
|
1386
|
+
content: [{ type: "text", text: `Local skills:\n${localText}\n\nHub skills:\n${hubText}` }],
|
|
1387
|
+
details: { query: skillQuery, scope: rawScope, local: { hits: localHits }, hub },
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
854
1390
|
|
|
855
1391
|
const hits = await engine.searchSkills(skillQuery, scope as any, currentOwner);
|
|
856
1392
|
|
|
@@ -862,7 +1398,7 @@ const memosLocalPlugin = {
|
|
|
862
1398
|
}
|
|
863
1399
|
|
|
864
1400
|
const text = hits.map((h, i) =>
|
|
865
|
-
`${i + 1}. [${h.name}] ${h.description}${h.visibility === "public" ? " (
|
|
1401
|
+
`${i + 1}. [${h.name}] ${h.description}${h.visibility === "public" ? " (shared to local agents)" : ""}`,
|
|
866
1402
|
).join("\n");
|
|
867
1403
|
|
|
868
1404
|
return {
|
|
@@ -880,20 +1416,54 @@ const memosLocalPlugin = {
|
|
|
880
1416
|
{
|
|
881
1417
|
name: "skill_publish",
|
|
882
1418
|
label: "Publish Skill",
|
|
883
|
-
description:
|
|
1419
|
+
description:
|
|
1420
|
+
"Share a skill with local agents or publish it to the Hub. " +
|
|
1421
|
+
"Use target='agents' for local sharing, or target='hub' with visibility='public'/'group' for Hub publishing. " +
|
|
1422
|
+
"The old scope parameter is still accepted for backward compatibility.",
|
|
884
1423
|
parameters: Type.Object({
|
|
885
1424
|
skillId: Type.String({ description: "The skill ID to publish" }),
|
|
1425
|
+
target: Type.Optional(Type.String({ description: "Publish target: 'agents' (default) or 'hub'." })),
|
|
1426
|
+
visibility: Type.Optional(Type.String({ description: "Hub visibility when target='hub': 'public' (default) or 'group'." })),
|
|
1427
|
+
scope: Type.Optional(Type.String({ description: "Deprecated alias: omit for local agents, or use 'public' / 'group' to publish to Hub." })),
|
|
1428
|
+
groupId: Type.Optional(Type.String({ description: "Optional group ID when scope='group'" })),
|
|
1429
|
+
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for tests or manual routing" })),
|
|
1430
|
+
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for tests" })),
|
|
886
1431
|
}),
|
|
887
1432
|
execute: trackTool("skill_publish", async (_toolCallId: any, params: any) => {
|
|
888
|
-
const {
|
|
1433
|
+
const {
|
|
1434
|
+
skillId: pubSkillId,
|
|
1435
|
+
target: rawTarget,
|
|
1436
|
+
visibility: rawVisibility,
|
|
1437
|
+
scope,
|
|
1438
|
+
groupId,
|
|
1439
|
+
hubAddress,
|
|
1440
|
+
userToken,
|
|
1441
|
+
} = params as {
|
|
1442
|
+
skillId: string;
|
|
1443
|
+
target?: string;
|
|
1444
|
+
visibility?: string;
|
|
1445
|
+
scope?: string;
|
|
1446
|
+
groupId?: string;
|
|
1447
|
+
hubAddress?: string;
|
|
1448
|
+
userToken?: string;
|
|
1449
|
+
};
|
|
889
1450
|
const skill = store.getSkill(pubSkillId);
|
|
890
1451
|
if (!skill) {
|
|
891
1452
|
return { content: [{ type: "text", text: `Skill not found: ${pubSkillId}` }] };
|
|
892
1453
|
}
|
|
1454
|
+
const target = resolveSkillPublishTarget(rawTarget, scope);
|
|
1455
|
+
const visibility = resolveSkillHubVisibility(rawVisibility, scope);
|
|
1456
|
+
if (target === "hub") {
|
|
1457
|
+
const published = await publishSkillBundleToHub(store, ctx, { skillId: pubSkillId, visibility, groupId, hubAddress, userToken });
|
|
1458
|
+
return {
|
|
1459
|
+
content: [{ type: "text", text: `Skill "${skill.name}" shared to Hub (${published.visibility}).` }],
|
|
1460
|
+
details: { skillId: pubSkillId, name: skill.name, target, publishedToHub: true, hubSkillId: published.skillId, visibility: published.visibility },
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
893
1463
|
store.setSkillVisibility(pubSkillId, "public");
|
|
894
1464
|
return {
|
|
895
|
-
content: [{ type: "text", text: `Skill "${skill.name}" is now
|
|
896
|
-
details: { skillId: pubSkillId, name: skill.name, visibility: "public" },
|
|
1465
|
+
content: [{ type: "text", text: `Skill "${skill.name}" is now shared with local agents.` }],
|
|
1466
|
+
details: { skillId: pubSkillId, name: skill.name, target, visibility: "public", publishedToHub: false },
|
|
897
1467
|
};
|
|
898
1468
|
}),
|
|
899
1469
|
},
|
|
@@ -906,30 +1476,78 @@ const memosLocalPlugin = {
|
|
|
906
1476
|
{
|
|
907
1477
|
name: "skill_unpublish",
|
|
908
1478
|
label: "Unpublish Skill",
|
|
909
|
-
description:
|
|
1479
|
+
description:
|
|
1480
|
+
"Stop sharing a skill with local agents, remove it from the Hub, or do both. " +
|
|
1481
|
+
"Use target='agents' (default), 'hub', or 'all'.",
|
|
910
1482
|
parameters: Type.Object({
|
|
911
1483
|
skillId: Type.String({ description: "The skill ID to unpublish" }),
|
|
1484
|
+
target: Type.Optional(Type.String({ description: "Unpublish target: 'agents' (default), 'hub', or 'all'." })),
|
|
1485
|
+
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for tests or manual routing" })),
|
|
1486
|
+
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for tests" })),
|
|
912
1487
|
}),
|
|
913
1488
|
execute: trackTool("skill_unpublish", async (_toolCallId: any, params: any) => {
|
|
914
|
-
const { skillId: unpubSkillId } = params as { skillId: string };
|
|
1489
|
+
const { skillId: unpubSkillId, target, hubAddress, userToken } = params as { skillId: string; target?: string; hubAddress?: string; userToken?: string };
|
|
915
1490
|
const skill = store.getSkill(unpubSkillId);
|
|
916
1491
|
if (!skill) {
|
|
917
1492
|
return { content: [{ type: "text", text: `Skill not found: ${unpubSkillId}` }] };
|
|
918
1493
|
}
|
|
919
|
-
|
|
1494
|
+
const resolvedTarget = resolveSkillUnpublishTarget(target);
|
|
1495
|
+
const messages: string[] = [];
|
|
1496
|
+
const details: Record<string, unknown> = { skillId: unpubSkillId, name: skill.name, target: resolvedTarget };
|
|
1497
|
+
if (resolvedTarget === "hub" || resolvedTarget === "all") {
|
|
1498
|
+
try {
|
|
1499
|
+
await unpublishSkillBundleFromHub(store, ctx, { skillId: unpubSkillId, hubAddress, userToken });
|
|
1500
|
+
details.hub = { unpublished: true };
|
|
1501
|
+
messages.push("removed from Hub sharing");
|
|
1502
|
+
} catch (err) {
|
|
1503
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1504
|
+
if (resolvedTarget === "all" && msg.includes("hub client connection is not configured")) {
|
|
1505
|
+
details.hub = { unpublished: false, skipped: true, reason: "hub_not_configured" };
|
|
1506
|
+
} else {
|
|
1507
|
+
throw err;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
if (resolvedTarget === "agents" || resolvedTarget === "all") {
|
|
1512
|
+
store.setSkillVisibility(unpubSkillId, "private");
|
|
1513
|
+
details.local = { visibility: "private" };
|
|
1514
|
+
messages.push("limited to this agent");
|
|
1515
|
+
}
|
|
920
1516
|
return {
|
|
921
|
-
content: [{ type: "text", text: `Skill "${skill.name}"
|
|
922
|
-
details
|
|
1517
|
+
content: [{ type: "text", text: `Skill "${skill.name}" ${messages.join(" and ")}.` }],
|
|
1518
|
+
details,
|
|
923
1519
|
};
|
|
924
1520
|
}),
|
|
925
1521
|
},
|
|
926
1522
|
{ name: "skill_unpublish" },
|
|
927
1523
|
);
|
|
928
1524
|
|
|
1525
|
+
api.registerTool(
|
|
1526
|
+
{
|
|
1527
|
+
name: "network_skill_pull",
|
|
1528
|
+
label: "Network Skill Pull",
|
|
1529
|
+
description: "Download a published Hub skill bundle and restore it into local managed skills.",
|
|
1530
|
+
parameters: Type.Object({
|
|
1531
|
+
skillId: Type.String({ description: "The Hub skill ID to pull" }),
|
|
1532
|
+
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for tests or manual routing" })),
|
|
1533
|
+
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for tests" })),
|
|
1534
|
+
}),
|
|
1535
|
+
execute: trackTool("network_skill_pull", async (_toolCallId: any, params: any) => {
|
|
1536
|
+
const { skillId, hubAddress, userToken } = params as { skillId: string; hubAddress?: string; userToken?: string };
|
|
1537
|
+
const payload = await fetchHubSkillBundle(store, ctx, { skillId, hubAddress, userToken });
|
|
1538
|
+
const restored = restoreSkillBundleFromHub(store, ctx, payload);
|
|
1539
|
+
return {
|
|
1540
|
+
content: [{ type: "text", text: `Pulled Hub skill "${restored.localName}" into local storage.` }],
|
|
1541
|
+
details: { pulled: true, hubSkillId: skillId, localSkillId: restored.localSkillId, localName: restored.localName, dirPath: restored.dirPath },
|
|
1542
|
+
};
|
|
1543
|
+
}),
|
|
1544
|
+
},
|
|
1545
|
+
{ name: "network_skill_pull" },
|
|
1546
|
+
);
|
|
1547
|
+
|
|
929
1548
|
// ─── Auto-recall: inject relevant memories before agent starts ───
|
|
930
1549
|
|
|
931
1550
|
api.on("before_agent_start", async (event: { prompt?: string; messages?: unknown[] }, hookCtx?: { agentId?: string; sessionKey?: string }) => {
|
|
932
|
-
if (!allowPromptInjection) return {};
|
|
933
1551
|
if (!event.prompt || event.prompt.length < 3) return;
|
|
934
1552
|
|
|
935
1553
|
const recallAgentId = hookCtx?.agentId ?? "main";
|
|
@@ -1231,11 +1849,74 @@ const memosLocalPlugin = {
|
|
|
1231
1849
|
worker.enqueue(captured);
|
|
1232
1850
|
telemetry.trackMemoryIngested(captured.length);
|
|
1233
1851
|
}
|
|
1852
|
+
|
|
1853
|
+
// Incremental push: sync new chunks for already-shared tasks
|
|
1854
|
+
syncSharedTasksIncremental().catch((err) => {
|
|
1855
|
+
ctx.log.warn(`incremental sync failed: ${err}`);
|
|
1856
|
+
});
|
|
1234
1857
|
} catch (err) {
|
|
1235
1858
|
api.logger.warn(`memos-local: capture failed: ${String(err)}`);
|
|
1236
1859
|
}
|
|
1237
1860
|
});
|
|
1238
1861
|
|
|
1862
|
+
async function syncSharedTasksIncremental(): Promise<void> {
|
|
1863
|
+
if (!ctx.config.sharing?.enabled || ctx.config.sharing.role !== "client") return;
|
|
1864
|
+
const shared = store.listLocalSharedTasks();
|
|
1865
|
+
if (shared.length === 0) return;
|
|
1866
|
+
|
|
1867
|
+
let hubClient: { hubUrl: string; userToken: string; userId: string } | undefined;
|
|
1868
|
+
try {
|
|
1869
|
+
hubClient = await resolveHubClient(store, ctx);
|
|
1870
|
+
} catch {
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
const { v4: uuidv4 } = require("uuid");
|
|
1874
|
+
|
|
1875
|
+
for (const entry of shared) {
|
|
1876
|
+
const task = store.getTask(entry.taskId);
|
|
1877
|
+
if (!task) continue;
|
|
1878
|
+
const chunks = store.getChunksByTask(entry.taskId);
|
|
1879
|
+
if (chunks.length <= entry.syncedChunks) continue;
|
|
1880
|
+
|
|
1881
|
+
const newChunks = chunks.slice(entry.syncedChunks);
|
|
1882
|
+
ctx.log.info(`incremental sync: task=${entry.taskId} pushing ${newChunks.length} new chunk(s)`);
|
|
1883
|
+
|
|
1884
|
+
try {
|
|
1885
|
+
await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/tasks/share", {
|
|
1886
|
+
method: "POST",
|
|
1887
|
+
body: JSON.stringify({
|
|
1888
|
+
task: {
|
|
1889
|
+
id: entry.hubTaskId,
|
|
1890
|
+
sourceTaskId: entry.taskId,
|
|
1891
|
+
sourceUserId: hubClient.userId,
|
|
1892
|
+
title: task.title,
|
|
1893
|
+
summary: task.summary,
|
|
1894
|
+
groupId: entry.visibility === "group" ? entry.groupId ?? null : null,
|
|
1895
|
+
visibility: entry.visibility,
|
|
1896
|
+
createdAt: task.startedAt ?? task.updatedAt ?? Date.now(),
|
|
1897
|
+
updatedAt: task.updatedAt ?? Date.now(),
|
|
1898
|
+
},
|
|
1899
|
+
chunks: newChunks.map((chunk) => ({
|
|
1900
|
+
id: uuidv4(),
|
|
1901
|
+
hubTaskId: entry.hubTaskId,
|
|
1902
|
+
sourceTaskId: entry.taskId,
|
|
1903
|
+
sourceChunkId: chunk.id,
|
|
1904
|
+
sourceUserId: hubClient.userId,
|
|
1905
|
+
role: chunk.role,
|
|
1906
|
+
content: chunk.content,
|
|
1907
|
+
summary: chunk.summary,
|
|
1908
|
+
kind: chunk.kind,
|
|
1909
|
+
createdAt: chunk.createdAt,
|
|
1910
|
+
})),
|
|
1911
|
+
}),
|
|
1912
|
+
});
|
|
1913
|
+
store.markTaskShared(entry.taskId, entry.hubTaskId, chunks.length, entry.visibility, entry.groupId);
|
|
1914
|
+
} catch (err) {
|
|
1915
|
+
ctx.log.warn(`incremental sync failed for task=${entry.taskId}: ${err}`);
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1239
1920
|
// ─── Memory Viewer (web UI) ───
|
|
1240
1921
|
|
|
1241
1922
|
const viewer = new ViewerServer({
|
|
@@ -1247,11 +1928,30 @@ const memosLocalPlugin = {
|
|
|
1247
1928
|
ctx,
|
|
1248
1929
|
});
|
|
1249
1930
|
|
|
1931
|
+
const hubServer = ctx.config.sharing?.enabled && ctx.config.sharing.role === "hub"
|
|
1932
|
+
? new HubServer({ store, log: ctx.log, config: ctx.config, dataDir: stateDir, embedder })
|
|
1933
|
+
: null;
|
|
1934
|
+
|
|
1250
1935
|
// ─── Service lifecycle ───
|
|
1251
1936
|
|
|
1252
1937
|
api.registerService({
|
|
1253
1938
|
id: "memos-local-openclaw-plugin",
|
|
1254
1939
|
start: async () => {
|
|
1940
|
+
if (hubServer) {
|
|
1941
|
+
const hubUrl = await hubServer.start();
|
|
1942
|
+
api.logger.info(`memos-local: hub started at ${hubUrl}`);
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
// Auto-connect to Hub in client mode (handles both existing token and auto-join via teamToken)
|
|
1946
|
+
if (ctx.config.sharing?.enabled && ctx.config.sharing.role === "client") {
|
|
1947
|
+
try {
|
|
1948
|
+
const session = await connectToHub(store, ctx.config, ctx.log);
|
|
1949
|
+
api.logger.info(`memos-local: connected to Hub as "${session.username}" (${session.userId})`);
|
|
1950
|
+
} catch (err) {
|
|
1951
|
+
api.logger.warn(`memos-local: Hub connection failed: ${err}`);
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1255
1955
|
try {
|
|
1256
1956
|
const viewerUrl = await viewer.start();
|
|
1257
1957
|
api.logger.info(`memos-local: started (embedding: ${embedder.provider})`);
|
|
@@ -1277,7 +1977,9 @@ const memosLocalPlugin = {
|
|
|
1277
1977
|
);
|
|
1278
1978
|
},
|
|
1279
1979
|
stop: async () => {
|
|
1980
|
+
await worker.flush();
|
|
1280
1981
|
await telemetry.shutdown();
|
|
1982
|
+
await hubServer?.stop();
|
|
1281
1983
|
viewer.stop();
|
|
1282
1984
|
store.close();
|
|
1283
1985
|
api.logger.info("memos-local: stopped");
|